mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* master: stream volume listings A listing of 800k volumes is 36MB on the wire but 305MB as messages, and the master built all of it, then held it while grpc encoded it. Two of those at once is most of a small master's heap, and the maintenance scanner asks every 30 minutes. The topology goes out first, listing nothing, then its volumes in batches, so the master holds a batch rather than a cluster: 341MB of live heap for one listing becomes 4.4MB. It allocates much the same either way -- what changes is how much of it has to be live at once, which is what sets the heap ceiling. Batches are built under their disk's lock and sent outside it, so a slow reader stalls the stream rather than the topology. They therefore do not share one instant, which a single listing did not either: it takes each disk's lock in turn, so a volume moving during either can be seen twice or not at all. The client helper hides which kind of master answered: one too old for the stream is asked the old way and its reply cut into the same batches. Either way the topology handed over lists no volumes, so a caller cannot come to depend on finding them there. * admin: stream the listing the maintenance scan reads It asks for every volume in the cluster every 30 minutes. Reassembling it client-side keeps the scan identical -- ActiveTopology splits disks by the disk ids on the volumes, so it needs them in the topology -- while the master no longer builds the whole reply to send it. * topology: report a disk id that does not depend on map order A topology disk that fronts several physical disks took its reported id from whichever volume the map yielded first, so two listings of an unchanged disk could disagree. Take the smallest instead. * topology: test that a streamed listing rebuilds to the whole one The callers that stream now rebuild the listing from a topology sent without volumes plus the batches after it, so that has to come out the same as being sent it whole, at every batch size and under a filter. * clients: stream the volume listings that ask for everything The dashboard's list and export pages, the collection and ec shard pages, the topology view, the worker metrics and two shell commands each asked the master to build all 800k volumes into one reply. They read the same listing as before, rebuilt on their side, so the master no longer holds it. The three that already ask for one volume or one collection stay as they are: their replies are small, and streaming one costs a round trip to say so.
683 lines
18 KiB
Go
683 lines
18 KiB
Go
package dash
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
|
|
)
|
|
|
|
// matchesCollection checks if a volume/EC volume collection matches the filter collection.
|
|
// Handles the special case where empty collection ("") represents the "default" collection.
|
|
func matchesCollection(volumeCollection, filterCollection string) bool {
|
|
// Handle special case where "default" filter matches empty collection
|
|
if filterCollection == "default" && volumeCollection == "" {
|
|
return true
|
|
}
|
|
// Direct string match for named collections
|
|
return volumeCollection == filterCollection
|
|
}
|
|
|
|
// GetClusterEcShards retrieves cluster EC shards data with pagination, sorting, and filtering
|
|
func (s *AdminServer) GetClusterEcShards(page int, pageSize int, sortBy string, sortOrder string, collection string) (*ClusterEcShardsData, error) {
|
|
// Set defaults
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 1000 {
|
|
pageSize = 100
|
|
}
|
|
if sortBy == "" {
|
|
sortBy = "volume_id"
|
|
}
|
|
if sortOrder == "" {
|
|
sortOrder = "asc"
|
|
}
|
|
|
|
var ecShards []EcShardWithInfo
|
|
volumeShardsMap := make(map[uint32]map[int]bool) // volumeId -> set of shards present
|
|
volumesWithAllShards := 0
|
|
volumesWithMissingShards := 0
|
|
|
|
// Get detailed EC shard information via gRPC
|
|
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
|
resp, err := pb.CollectVolumeList(context.Background(), client, &master_pb.VolumeListRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.TopologyInfo != nil {
|
|
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, diskInfo := range node.DiskInfos {
|
|
// Process EC shard information
|
|
for _, ecShardInfo := range diskInfo.EcShardInfos {
|
|
volumeId := ecShardInfo.Id
|
|
|
|
// Initialize volume shards map if needed
|
|
if volumeShardsMap[volumeId] == nil {
|
|
volumeShardsMap[volumeId] = make(map[int]bool)
|
|
}
|
|
|
|
// Create individual shard entries for each shard this server has
|
|
shardBits := ecShardInfo.EcIndexBits
|
|
for shardId := 0; shardId < erasure_coding.MaxShardCount; shardId++ {
|
|
if (shardBits & (1 << uint(shardId))) != 0 {
|
|
// Mark this shard as present for this volume
|
|
volumeShardsMap[volumeId][shardId] = true
|
|
|
|
ecShard := EcShardWithInfo{
|
|
VolumeID: volumeId,
|
|
ShardID: uint32(shardId),
|
|
Collection: ecShardInfo.Collection,
|
|
Size: 0, // EC shards don't have individual size in the API response
|
|
Server: node.Id,
|
|
DataCenter: dc.Id,
|
|
Rack: rack.Id,
|
|
DiskType: diskInfo.Type,
|
|
ModifiedTime: 0, // Not available in current API
|
|
EcIndexBits: ecShardInfo.EcIndexBits,
|
|
ShardCount: getShardCount(ecShardInfo.EcIndexBits),
|
|
}
|
|
ecShards = append(ecShards, ecShard)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate volume-level completeness (across all servers)
|
|
volumeCompleteness := make(map[uint32]bool)
|
|
volumeMissingShards := make(map[uint32][]int)
|
|
|
|
for volumeId, shardsPresent := range volumeShardsMap {
|
|
var missingShards []int
|
|
shardCount := len(shardsPresent)
|
|
|
|
// Find which shards are missing for this volume across ALL servers
|
|
// Uses default 10+4 (14 total shards)
|
|
for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ {
|
|
if !shardsPresent[shardId] {
|
|
missingShards = append(missingShards, shardId)
|
|
}
|
|
}
|
|
|
|
isComplete := (shardCount == erasure_coding.TotalShardsCount)
|
|
volumeCompleteness[volumeId] = isComplete
|
|
volumeMissingShards[volumeId] = missingShards
|
|
|
|
if isComplete {
|
|
volumesWithAllShards++
|
|
} else {
|
|
volumesWithMissingShards++
|
|
}
|
|
}
|
|
|
|
// Update completeness info for each shard based on volume-level completeness
|
|
for i := range ecShards {
|
|
volumeId := ecShards[i].VolumeID
|
|
ecShards[i].IsComplete = volumeCompleteness[volumeId]
|
|
ecShards[i].MissingShards = volumeMissingShards[volumeId]
|
|
}
|
|
|
|
// Filter by collection if specified
|
|
if collection != "" {
|
|
var filteredShards []EcShardWithInfo
|
|
for _, shard := range ecShards {
|
|
if shard.Collection == collection {
|
|
filteredShards = append(filteredShards, shard)
|
|
}
|
|
}
|
|
ecShards = filteredShards
|
|
}
|
|
|
|
// Sort the results
|
|
sortEcShards(ecShards, sortBy, sortOrder)
|
|
|
|
// Calculate statistics for conditional display
|
|
dataCenters := make(map[string]bool)
|
|
racks := make(map[string]bool)
|
|
collections := make(map[string]bool)
|
|
|
|
for _, shard := range ecShards {
|
|
dataCenters[shard.DataCenter] = true
|
|
racks[shard.Rack] = true
|
|
if shard.Collection != "" {
|
|
collections[shard.Collection] = true
|
|
}
|
|
}
|
|
|
|
// Pagination
|
|
totalShards := len(ecShards)
|
|
totalPages := (totalShards + pageSize - 1) / pageSize
|
|
startIndex := (page - 1) * pageSize
|
|
endIndex := startIndex + pageSize
|
|
if endIndex > totalShards {
|
|
endIndex = totalShards
|
|
}
|
|
|
|
if startIndex >= totalShards {
|
|
startIndex = 0
|
|
endIndex = 0
|
|
}
|
|
|
|
paginatedShards := ecShards[startIndex:endIndex]
|
|
|
|
// Build response
|
|
data := &ClusterEcShardsData{
|
|
EcShards: paginatedShards,
|
|
TotalShards: totalShards,
|
|
TotalVolumes: len(volumeShardsMap),
|
|
LastUpdated: time.Now(),
|
|
|
|
// Pagination
|
|
CurrentPage: page,
|
|
TotalPages: totalPages,
|
|
PageSize: pageSize,
|
|
|
|
// Sorting
|
|
SortBy: sortBy,
|
|
SortOrder: sortOrder,
|
|
|
|
// Statistics
|
|
DataCenterCount: len(dataCenters),
|
|
RackCount: len(racks),
|
|
CollectionCount: len(collections),
|
|
|
|
// Conditional display flags
|
|
ShowDataCenterColumn: len(dataCenters) > 1,
|
|
ShowRackColumn: len(racks) > 1,
|
|
ShowCollectionColumn: len(collections) > 1 || collection != "",
|
|
|
|
// Filtering
|
|
FilterCollection: collection,
|
|
|
|
// EC specific statistics
|
|
ShardsPerVolume: make(map[uint32]int), // This will be recalculated below
|
|
VolumesWithAllShards: volumesWithAllShards,
|
|
VolumesWithMissingShards: volumesWithMissingShards,
|
|
}
|
|
|
|
// Recalculate ShardsPerVolume for the response
|
|
for volumeId, shardsPresent := range volumeShardsMap {
|
|
data.ShardsPerVolume[volumeId] = len(shardsPresent)
|
|
}
|
|
|
|
// Set single values when only one exists
|
|
if len(dataCenters) == 1 {
|
|
for dc := range dataCenters {
|
|
data.SingleDataCenter = dc
|
|
break
|
|
}
|
|
}
|
|
if len(racks) == 1 {
|
|
for rack := range racks {
|
|
data.SingleRack = rack
|
|
break
|
|
}
|
|
}
|
|
if len(collections) == 1 {
|
|
for col := range collections {
|
|
data.SingleCollection = col
|
|
break
|
|
}
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// GetClusterEcVolumes retrieves cluster EC volumes data grouped by volume ID with shard locations
|
|
func (s *AdminServer) GetClusterEcVolumes(page int, pageSize int, sortBy string, sortOrder string, collection string) (*ClusterEcVolumesData, error) {
|
|
// Set defaults
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 1000 {
|
|
pageSize = 100
|
|
}
|
|
if sortBy == "" {
|
|
sortBy = "volume_id"
|
|
}
|
|
if sortOrder == "" {
|
|
sortOrder = "asc"
|
|
}
|
|
|
|
volumeData := make(map[uint32]*EcVolumeWithShards)
|
|
totalShards := 0
|
|
|
|
// Get detailed EC shard information via gRPC
|
|
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
|
resp, err := pb.CollectVolumeList(context.Background(), client, &master_pb.VolumeListRequest{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.TopologyInfo != nil {
|
|
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, diskInfo := range node.DiskInfos {
|
|
// Process EC shard information
|
|
for _, ecShardInfo := range diskInfo.EcShardInfos {
|
|
volumeId := ecShardInfo.Id
|
|
|
|
// Initialize volume data if needed
|
|
if volumeData[volumeId] == nil {
|
|
volumeData[volumeId] = &EcVolumeWithShards{
|
|
VolumeID: volumeId,
|
|
Collection: ecShardInfo.Collection,
|
|
TotalShards: 0,
|
|
IsComplete: false,
|
|
MissingShards: []int{},
|
|
ShardLocations: make(map[int]string),
|
|
ShardSizes: make(map[int]int64),
|
|
DataCenters: []string{},
|
|
Servers: []string{},
|
|
Racks: []string{},
|
|
}
|
|
}
|
|
|
|
volume := volumeData[volumeId]
|
|
|
|
// Track data centers and servers
|
|
dcExists := false
|
|
for _, existingDc := range volume.DataCenters {
|
|
if existingDc == dc.Id {
|
|
dcExists = true
|
|
break
|
|
}
|
|
}
|
|
if !dcExists {
|
|
volume.DataCenters = append(volume.DataCenters, dc.Id)
|
|
}
|
|
|
|
serverExists := false
|
|
for _, existingServer := range volume.Servers {
|
|
if existingServer == node.Id {
|
|
serverExists = true
|
|
break
|
|
}
|
|
}
|
|
if !serverExists {
|
|
volume.Servers = append(volume.Servers, node.Id)
|
|
}
|
|
|
|
// Track racks
|
|
rackExists := false
|
|
for _, existingRack := range volume.Racks {
|
|
if existingRack == rack.Id {
|
|
rackExists = true
|
|
break
|
|
}
|
|
}
|
|
if !rackExists {
|
|
volume.Racks = append(volume.Racks, rack.Id)
|
|
}
|
|
|
|
// Process each shard this server has for this volume
|
|
shardBits := ecShardInfo.EcIndexBits
|
|
shardSizes := ecShardInfo.ShardSizes
|
|
sizeIndex := 0
|
|
for shardId := 0; shardId < erasure_coding.MaxShardCount; shardId++ {
|
|
if (shardBits & (1 << uint(shardId))) != 0 {
|
|
// Record shard location
|
|
volume.ShardLocations[shardId] = node.Id
|
|
if sizeIndex < len(shardSizes) {
|
|
size := shardSizes[sizeIndex]
|
|
if size >= 0 {
|
|
volume.ShardSizes[shardId] = size
|
|
}
|
|
}
|
|
sizeIndex++
|
|
totalShards++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Calculate completeness for each volume
|
|
completeVolumes := 0
|
|
incompleteVolumes := 0
|
|
|
|
for _, volume := range volumeData {
|
|
volume.TotalShards = len(volume.ShardLocations)
|
|
|
|
// Find missing shards (default 10+4 = 14 total shards)
|
|
var missingShards []int
|
|
for shardId := 0; shardId < erasure_coding.TotalShardsCount; shardId++ {
|
|
if _, exists := volume.ShardLocations[shardId]; !exists {
|
|
missingShards = append(missingShards, shardId)
|
|
}
|
|
}
|
|
|
|
volume.MissingShards = missingShards
|
|
volume.IsComplete = (len(missingShards) == 0)
|
|
|
|
if volume.IsComplete {
|
|
completeVolumes++
|
|
} else {
|
|
incompleteVolumes++
|
|
}
|
|
}
|
|
|
|
// Convert map to slice
|
|
var ecVolumes []EcVolumeWithShards
|
|
for _, volume := range volumeData {
|
|
// Filter by collection if specified
|
|
if collection == "" || matchesCollection(volume.Collection, collection) {
|
|
ecVolumes = append(ecVolumes, *volume)
|
|
}
|
|
}
|
|
|
|
// Sort the results
|
|
sortEcVolumes(ecVolumes, sortBy, sortOrder)
|
|
|
|
// Calculate statistics for conditional display
|
|
dataCenters := make(map[string]bool)
|
|
collections := make(map[string]bool)
|
|
|
|
for _, volume := range ecVolumes {
|
|
for _, dc := range volume.DataCenters {
|
|
dataCenters[dc] = true
|
|
}
|
|
if volume.Collection != "" {
|
|
collections[volume.Collection] = true
|
|
}
|
|
}
|
|
|
|
// Pagination
|
|
totalVolumes := len(ecVolumes)
|
|
totalPages := (totalVolumes + pageSize - 1) / pageSize
|
|
startIndex := (page - 1) * pageSize
|
|
endIndex := startIndex + pageSize
|
|
if endIndex > totalVolumes {
|
|
endIndex = totalVolumes
|
|
}
|
|
|
|
if startIndex >= totalVolumes {
|
|
startIndex = 0
|
|
endIndex = 0
|
|
}
|
|
|
|
paginatedVolumes := ecVolumes[startIndex:endIndex]
|
|
|
|
// Build response
|
|
data := &ClusterEcVolumesData{
|
|
EcVolumes: paginatedVolumes,
|
|
TotalVolumes: totalVolumes,
|
|
LastUpdated: time.Now(),
|
|
|
|
// Pagination
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
TotalPages: totalPages,
|
|
|
|
// Sorting
|
|
SortBy: sortBy,
|
|
SortOrder: sortOrder,
|
|
|
|
// Filtering
|
|
Collection: collection,
|
|
|
|
// Conditional display flags
|
|
ShowDataCenterColumn: len(dataCenters) > 1,
|
|
ShowRackColumn: false, // We don't track racks in this view for simplicity
|
|
ShowCollectionColumn: len(collections) > 1 || collection != "",
|
|
|
|
// Statistics
|
|
CompleteVolumes: completeVolumes,
|
|
IncompleteVolumes: incompleteVolumes,
|
|
TotalShards: totalShards,
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// sortEcVolumes sorts EC volumes based on the specified field and order
|
|
func sortEcVolumes(volumes []EcVolumeWithShards, sortBy string, sortOrder string) {
|
|
sort.Slice(volumes, func(i, j int) bool {
|
|
var less bool
|
|
switch sortBy {
|
|
case "volume_id":
|
|
less = volumes[i].VolumeID < volumes[j].VolumeID
|
|
case "collection":
|
|
if volumes[i].Collection == volumes[j].Collection {
|
|
less = volumes[i].VolumeID < volumes[j].VolumeID
|
|
} else {
|
|
less = volumes[i].Collection < volumes[j].Collection
|
|
}
|
|
case "total_shards":
|
|
if volumes[i].TotalShards == volumes[j].TotalShards {
|
|
less = volumes[i].VolumeID < volumes[j].VolumeID
|
|
} else {
|
|
less = volumes[i].TotalShards < volumes[j].TotalShards
|
|
}
|
|
case "completeness":
|
|
// Complete volumes first, then by volume ID
|
|
if volumes[i].IsComplete == volumes[j].IsComplete {
|
|
less = volumes[i].VolumeID < volumes[j].VolumeID
|
|
} else {
|
|
less = volumes[i].IsComplete && !volumes[j].IsComplete
|
|
}
|
|
default:
|
|
less = volumes[i].VolumeID < volumes[j].VolumeID
|
|
}
|
|
|
|
if sortOrder == "desc" {
|
|
return !less
|
|
}
|
|
return less
|
|
})
|
|
}
|
|
|
|
// getShardCount returns the number of shards represented by the bitmap
|
|
func getShardCount(ecIndexBits uint32) int {
|
|
count := 0
|
|
for i := 0; i < erasure_coding.MaxShardCount; i++ {
|
|
if (ecIndexBits & (1 << uint(i))) != 0 {
|
|
count++
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
// sortEcShards sorts EC shards based on the specified field and order
|
|
func sortEcShards(shards []EcShardWithInfo, sortBy string, sortOrder string) {
|
|
sort.Slice(shards, func(i, j int) bool {
|
|
var less bool
|
|
switch sortBy {
|
|
case "shard_id":
|
|
less = shards[i].ShardID < shards[j].ShardID
|
|
case "server":
|
|
if shards[i].Server == shards[j].Server {
|
|
less = shards[i].ShardID < shards[j].ShardID // Secondary sort by shard ID
|
|
} else {
|
|
less = shards[i].Server < shards[j].Server
|
|
}
|
|
case "data_center":
|
|
if shards[i].DataCenter == shards[j].DataCenter {
|
|
less = shards[i].ShardID < shards[j].ShardID // Secondary sort by shard ID
|
|
} else {
|
|
less = shards[i].DataCenter < shards[j].DataCenter
|
|
}
|
|
case "rack":
|
|
if shards[i].Rack == shards[j].Rack {
|
|
less = shards[i].ShardID < shards[j].ShardID // Secondary sort by shard ID
|
|
} else {
|
|
less = shards[i].Rack < shards[j].Rack
|
|
}
|
|
default:
|
|
less = shards[i].ShardID < shards[j].ShardID
|
|
}
|
|
|
|
if sortOrder == "desc" {
|
|
return !less
|
|
}
|
|
return less
|
|
})
|
|
}
|
|
|
|
// GetEcVolumeDetails retrieves detailed information about a specific EC volume
|
|
func (s *AdminServer) GetEcVolumeDetails(volumeID uint32, sortBy string, sortOrder string) (*EcVolumeDetailsData, error) {
|
|
// Set defaults
|
|
if sortBy == "" {
|
|
sortBy = "shard_id"
|
|
}
|
|
if sortOrder == "" {
|
|
sortOrder = "asc"
|
|
}
|
|
|
|
var shards []EcShardWithInfo
|
|
var collection string
|
|
dataCenters := make(map[string]bool)
|
|
servers := make(map[string]bool)
|
|
|
|
// Get detailed EC shard information for the specific volume via gRPC
|
|
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error {
|
|
resp, err := client.VolumeList(context.Background(), &master_pb.VolumeListRequest{VolumeId: volumeID})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if resp.TopologyInfo != nil {
|
|
for _, dc := range resp.TopologyInfo.DataCenterInfos {
|
|
for _, rack := range dc.RackInfos {
|
|
for _, node := range rack.DataNodeInfos {
|
|
for _, diskInfo := range node.DiskInfos {
|
|
// Process EC shard information for this specific volume
|
|
for _, ecShardInfo := range diskInfo.EcShardInfos {
|
|
// An older master ignores the filter.
|
|
if ecShardInfo.Id == volumeID {
|
|
collection = ecShardInfo.Collection
|
|
dataCenters[dc.Id] = true
|
|
servers[node.Id] = true
|
|
|
|
// Create individual shard entries for each shard this server has
|
|
shardBits := ecShardInfo.EcIndexBits
|
|
shardSizes := ecShardInfo.ShardSizes
|
|
sizeIndex := 0
|
|
for shardId := 0; shardId < erasure_coding.MaxShardCount; shardId++ {
|
|
if (shardBits & (1 << uint(shardId))) != 0 {
|
|
var shardSize uint64
|
|
if sizeIndex < len(shardSizes) {
|
|
size := shardSizes[sizeIndex]
|
|
if size >= 0 {
|
|
shardSize = uint64(size)
|
|
}
|
|
}
|
|
sizeIndex++
|
|
ecShard := EcShardWithInfo{
|
|
VolumeID: ecShardInfo.Id,
|
|
ShardID: uint32(shardId),
|
|
Collection: ecShardInfo.Collection,
|
|
Size: shardSize,
|
|
Server: node.Id,
|
|
DataCenter: dc.Id,
|
|
Rack: rack.Id,
|
|
DiskType: diskInfo.Type,
|
|
ModifiedTime: 0, // Not available in current API
|
|
EcIndexBits: ecShardInfo.EcIndexBits,
|
|
ShardCount: getShardCount(ecShardInfo.EcIndexBits),
|
|
}
|
|
shards = append(shards, ecShard)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(shards) == 0 {
|
|
return nil, fmt.Errorf("EC volume %d not found", volumeID)
|
|
}
|
|
|
|
// Calculate completeness based on unique shard IDs
|
|
foundShards := make(map[int]bool)
|
|
for _, shard := range shards {
|
|
foundShards[int(shard.ShardID)] = true
|
|
}
|
|
|
|
totalUniqueShards := len(foundShards)
|
|
// Check completeness using default 10+4 (14 total shards)
|
|
isComplete := (totalUniqueShards == erasure_coding.TotalShardsCount)
|
|
|
|
// Calculate missing shards
|
|
var missingShards []int
|
|
for i := 0; i < erasure_coding.TotalShardsCount; i++ {
|
|
if !foundShards[i] {
|
|
missingShards = append(missingShards, i)
|
|
}
|
|
}
|
|
|
|
// Update completeness info for each shard
|
|
for i := range shards {
|
|
shards[i].IsComplete = isComplete
|
|
shards[i].MissingShards = missingShards
|
|
}
|
|
|
|
// Sort shards based on parameters
|
|
sortEcShards(shards, sortBy, sortOrder)
|
|
|
|
// Convert maps to slices
|
|
var dcList []string
|
|
for dc := range dataCenters {
|
|
dcList = append(dcList, dc)
|
|
}
|
|
var serverList []string
|
|
for server := range servers {
|
|
serverList = append(serverList, server)
|
|
}
|
|
|
|
data := &EcVolumeDetailsData{
|
|
VolumeID: volumeID,
|
|
Collection: collection,
|
|
Shards: shards,
|
|
TotalShards: totalUniqueShards,
|
|
IsComplete: isComplete,
|
|
MissingShards: missingShards,
|
|
DataCenters: dcList,
|
|
Servers: serverList,
|
|
LastUpdated: time.Now(),
|
|
SortBy: sortBy,
|
|
SortOrder: sortOrder,
|
|
}
|
|
|
|
return data, nil
|
|
}
|