package topology import ( "testing" "github.com/seaweedfs/seaweedfs/weed/pb/master_pb" "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding" ) // referenceCollectionStatistics is what callers computed for themselves from a // full topology listing, kept here so the summary can be held to producing the // same numbers. Quota enforcement reads these. func referenceCollectionStatistics(t *master_pb.TopologyInfo) map[string]*CollectionStatistics { type volumeKey struct { collection string volumeId uint32 } out := map[string]*CollectionStatistics{} seen := map[volumeKey]bool{} ecCounts := map[volumeKey]*ecFileCounts{} statsFor := func(c string) *CollectionStatistics { s, ok := out[c] if !ok { s = &CollectionStatistics{Collection: c} out[c] = s } return s } for _, dc := range t.DataCenterInfos { for _, r := range dc.RackInfos { for _, dn := range r.DataNodeInfos { for _, diskInfo := range dn.DiskInfos { for _, vi := range diskInfo.VolumeInfos { s := statsFor(vi.Collection) s.PhysicalSize += vi.Size key := volumeKey{vi.Collection, vi.Id} if seen[key] { continue } seen[key] = true s.Size += vi.Size s.FileCount += vi.FileCount s.DeleteCount += vi.DeleteCount s.DeletedByteCount += vi.DeletedByteCount s.VolumeCount++ } for _, esi := range diskInfo.EcShardInfos { s := statsFor(esi.Collection) s.PhysicalSize += uint64(erasure_coding.EcShardsTotalSize(esi)) s.Size += uint64(erasure_coding.EcShardsDataSize(esi, 0)) key := volumeKey{esi.Collection, esi.Id} agg, ok := ecCounts[key] if !ok { agg = &ecFileCounts{collection: esi.Collection} ecCounts[key] = agg s.VolumeCount++ } if esi.FileCount > agg.fileCount { agg.fileCount = esi.FileCount } agg.deleteCount += esi.DeleteCount } } } } } for _, agg := range ecCounts { if s := out[agg.collection]; s != nil { s.FileCount += agg.fileCount s.DeleteCount += agg.deleteCount } } return out } func statsTopology(t *testing.T) *Topology { t.Helper() topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1") nodes := make([]*DataNode, 3) for i := range nodes { nodes[i] = rack.GetOrCreateDataNode( "10.0.0."+string(rune('1'+i)), 8080, 18080, "", "", map[string]uint32{"": 1000, "ssd": 1000}) } // Two collections, replicated volumes, one volume on a second disk type. // Replicas agree here; disagreement is covered on its own, where the // listing this is compared against is itself order dependent. report := func(dn *DataNode, msgs ...*master_pb.VolumeInformationMessage) { topo.SyncDataNodeRegistration(msgs, dn) } vol := func(id uint32, collection string, size uint64, diskType string) *master_pb.VolumeInformationMessage { return &master_pb.VolumeInformationMessage{ Id: id, Collection: collection, Size: size, FileCount: uint64(id) * 10, DeleteCount: uint64(id), DeletedByteCount: uint64(id) * 100, Version: 3, ReplicaPlacement: 1, DiskType: diskType, } } report(nodes[0], vol(1, "bucket-a", 1000, ""), vol(2, "bucket-a", 2000, ""), vol(5, "bucket-b", 700, "ssd")) report(nodes[1], vol(1, "bucket-a", 1000, ""), vol(3, "bucket-b", 3000, "")) report(nodes[2], vol(2, "bucket-a", 2000, ""), vol(3, "bucket-b", 3000, "")) // Ec shards for a third collection, spread over two nodes, with the file // count reported differently by each holder. topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{ {Id: 9, Collection: "bucket-c", EcIndexBits: 0x1f, ShardSizes: []int64{10, 20, 30, 40, 50}, FileCount: 40, DeleteCount: 2}, }, nodes[0]) topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{ {Id: 9, Collection: "bucket-c", EcIndexBits: 0x3e0, ShardSizes: []int64{60, 70, 80, 90, 100}, FileCount: 44, DeleteCount: 3}, }, nodes[1]) return topo } // The summary replaces callers adding up a full topology listing, so it has to // produce what that produced: these numbers enforce bucket quotas. func TestCollectionStatisticsMatchesAFullListing(t *testing.T) { topo := statsTopology(t) want := referenceCollectionStatistics(topo.ToTopologyInfo(VolumeFilter{})) got := map[string]*CollectionStatistics{} for _, s := range topo.CollectionStatistics() { got[s.Collection] = s } if len(got) != len(want) { t.Fatalf("summarised %d collections, the listing had %d: %v vs %v", len(got), len(want), got, want) } for name, expected := range want { actual, found := got[name] if !found { t.Errorf("collection %s is missing from the summary", name) continue } if *actual != *expected { t.Errorf("collection %s:\n summary %+v\n listing %+v", name, *actual, *expected) } } } // Replicas disagree while a write is landing or a heartbeat is late. The full // listing walked the topology in map order and took whichever replica it // reached first, so its answer was not stable; the summary takes the largest. func TestCollectionStatisticsPicksTheLargestReplica(t *testing.T) { topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1") small := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100}) large := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100}) behind := &master_pb.VolumeInformationMessage{ Id: 1, Collection: "bucket-a", Size: 1000, FileCount: 10, Version: 3, ReplicaPlacement: 1, } ahead := &master_pb.VolumeInformationMessage{ Id: 1, Collection: "bucket-a", Size: 4000, FileCount: 40, Version: 3, ReplicaPlacement: 1, } topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{behind}, small) topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{ahead}, large) for i := 0; i < 8; i++ { stats := topo.CollectionStatistics() if len(stats) != 1 { t.Fatalf("expected one collection, got %d", len(stats)) } if stats[0].Size != 4000 || stats[0].FileCount != 40 { t.Fatalf("run %d reported size %d files %d, want the larger replica's 4000 and 40", i, stats[0].Size, stats[0].FileCount) } if stats[0].PhysicalSize != 5000 { t.Fatalf("run %d reported physical size %d, want both replicas summed", i, stats[0].PhysicalSize) } } } func statsByCollection(topo *Topology) map[string]*CollectionStatistics { out := map[string]*CollectionStatistics{} for _, s := range topo.CollectionStatistics() { out[s.Collection] = s } return out } // The .ecx and .ecj travel with the shards, so a holder still loading them // reports zero and must not pin the volume's count down. func TestCollectionStatisticsTakesTheLargestEcFileCount(t *testing.T) { topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1") loading := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100}) loaded := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100}) topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{ {Id: 11, Collection: "bucket-b", EcIndexBits: 0x7f, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 0}, }, loading) topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{ {Id: 11, Collection: "bucket-b", EcIndexBits: 0x3f80, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 6}, }, loaded) stats := statsByCollection(topo)["bucket-b"] if stats == nil { t.Fatal("expected bucket-b to be reported") } if stats.FileCount != 6 { t.Errorf("file count %d, want the largest any holder reported (6)", stats.FileCount) } if stats.VolumeCount != 1 { t.Errorf("volume count %d, want one volume however many holders report shards", stats.VolumeCount) } // 14 shards of 1 byte each; only the 10 data shards count as logical. if stats.PhysicalSize != 14 { t.Errorf("physical size %d, want every shard counted (14)", stats.PhysicalSize) } if stats.Size != 10 { t.Errorf("size %d, want the data shards only (10)", stats.Size) } } // A collection with both kinds has to have them added together. func TestCollectionStatisticsCountsRegularAndEcTogether(t *testing.T) { topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1") dn := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100}) topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{ {Id: 1, Collection: "mixed", Size: 500, FileCount: 5, DeleteCount: 1, DeletedByteCount: 50, Version: 3}, }, dn) topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{ {Id: 2, Collection: "mixed", EcIndexBits: 0x3fff, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, FileCount: 7, DeleteCount: 2}, }, dn) stats := statsByCollection(topo)["mixed"] if stats == nil { t.Fatal("expected the mixed collection to be reported") } if stats.VolumeCount != 2 { t.Errorf("volume count %d, want the regular and the ec volume (2)", stats.VolumeCount) } if stats.FileCount != 12 { t.Errorf("file count %d, want 5 regular plus 7 ec", stats.FileCount) } if stats.DeleteCount != 3 { t.Errorf("delete count %d, want 1 regular plus 2 ec", stats.DeleteCount) } if stats.Size != 510 { t.Errorf("size %d, want 500 regular plus 10 data shards", stats.Size) } if stats.PhysicalSize != 514 { t.Errorf("physical size %d, want 500 regular plus all 14 shards", stats.PhysicalSize) } } // Quotas are enforced on size less deletions, so the replica holding the most // live data is the one to count. The replica with the biggest raw size can be // the one that has deleted the most, and counting that one would report a // bucket smaller than it is and leave it writable over its quota. func TestCollectionStatisticsPicksTheReplicaHoldingTheMostLiveData(t *testing.T) { topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1") bigMostlyDeleted := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100}) smallerButLive := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100}) topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{ Id: 1, Collection: "bucket-a", Size: 1000, DeletedByteCount: 900, Version: 3, ReplicaPlacement: 1, }}, bigMostlyDeleted) topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{ Id: 1, Collection: "bucket-a", Size: 900, DeletedByteCount: 0, Version: 3, ReplicaPlacement: 1, }}, smallerButLive) stats := statsByCollection(topo)["bucket-a"] if stats == nil { t.Fatal("expected bucket-a to be reported") } live := stats.Size - stats.DeletedByteCount if live != 900 { t.Errorf("reported %d bytes live (size %d less %d deleted), want the 900 one replica holds", live, stats.Size, stats.DeletedByteCount) } if stats.PhysicalSize != 1900 { t.Errorf("physical size %d, want both replicas summed", stats.PhysicalSize) } } // Live usage is read as the collection's size less its deletions, so a volume // reporting more deleted bytes than it holds must not cancel live bytes // belonging to other volumes in the same bucket. func TestCollectionStatisticsDeletionsNeverExceedTheVolume(t *testing.T) { topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false) dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1"). GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100}) topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{ // More deleted bytes than the volume holds, which a compaction can // leave behind, and a healthy volume beside it. {Id: 1, Collection: "bucket-a", Size: 100, DeletedByteCount: 500, Version: 3}, {Id: 2, Collection: "bucket-a", Size: 1000, DeletedByteCount: 0, Version: 3}, }, dn) stats := statsByCollection(topo)["bucket-a"] if stats == nil { t.Fatal("expected bucket-a to be reported") } if stats.DeletedByteCount > stats.Size { t.Errorf("deletions %d exceed size %d, so live usage reads as zero", stats.DeletedByteCount, stats.Size) } if live := stats.Size - stats.DeletedByteCount; live != 1000 { t.Errorf("reported %d bytes live, want the 1000 the second volume holds", live) } }