mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-16 19:40:43 +02:00
* fix(admin): list all masters and dedupe EC file counts in dashboard Dashboard -> Master Nodes only ever showed the currently connected master because getMasterNodesStatus hard-coded a single entry. Replace it with a RaftListClusterServers call that returns every master in the raft group and tags the real leader, falling back to the current master only if the raft call fails. Buckets -> Object Store Buckets could render 0 objects for a bucket backed by an EC volume. Every shard holder reports the same whole-volume file_count (read from the replicated .ecx), so the first-seen value wins; if that first node had not yet finished loading .ecx it reported 0 and pinned the aggregate at 0. Take the max across reporting nodes instead. The dashboard header total_files also dropped after volumes were converted to erasure coding because getTopologyViaGRPC never folded EC file_count into topology.TotalFiles. Aggregate it with the same max/sum dedupe. * fix(admin): address PR review comments - bound RaftListClusterServers with a 3s timeout so the dashboard endpoint cannot hang on a stalled master - pre-validate raft addresses with net.SplitHostPort before calling pb.GrpcAddressToServerAddress, which otherwise glog.Fatalf's on a malformed entry and would crash the admin process - when raft is unreachable, mark the fallback master as not-leader rather than claiming leadership the code cannot verify - warn when summed EC delete_count exceeds file_count while folding into topology.TotalFiles, matching collectCollectionStats * fix(admin): distinguish empty raft response from RPC failure When RaftListClusterServers returns successfully with no servers, raft is not initialized (standalone/non-raft cluster), so the single fallback master is the leader. Only treat the fallback as a non-leader when the RPC actually failed. * fix(admin): remove misleading Objects column from S3 buckets page The bucket "Objects" column displayed needle counts from volume collection stats, not actual S3 object counts. This is confusing because a single S3 object can span multiple needles (multipart uploads, versions) and the count is inaccurate for EC volumes. Remove the ObjectCount field from S3Bucket, the Objects table column, the sort-by-objects handler, the detail-view row, and both CSV export references. * fix(admin): correct cell indexes in fallback bucket CSV export After the Objects column was removed, the fallback CSV exporter in admin.js still used stale cell indexes: cells[1] mapped to Owner (not Created), cells[2] to Created (not Size), cells[3] to Logical Size (not Quota). Align all indexes with the current table column order and include Owner, Logical Size, and Physical Size.
238 lines
7.0 KiB
Go
238 lines
7.0 KiB
Go
package dash
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||
)
|
||
|
||
// TestCollectCollectionStatsECUnevenShards verifies that EC shards spread
|
||
// across multiple disks are aggregated correctly, including the case where
|
||
// the last data shard is smaller than the rest.
|
||
func TestCollectCollectionStatsECUnevenShards(t *testing.T) {
|
||
// Standard 10+4 EC layout. Shards 0..9 are data, 10..13 are parity.
|
||
// Data shards 0..8 are 1000 bytes; data shard 9 is 500 bytes (uneven tail).
|
||
// Parity shards 10..13 are 1000 bytes each.
|
||
//
|
||
// Physical (raw) = 9*1000 + 500 + 4*1000 = 13500
|
||
// Logical (data only) = 9*1000 + 500 = 9500
|
||
|
||
nodeA := &master_pb.DataNodeInfo{
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"disk1": {
|
||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||
{
|
||
Id: 42,
|
||
Collection: "bucket-a",
|
||
// Shards 0..6 held here.
|
||
EcIndexBits: (1 << 0) | (1 << 1) | (1 << 2) | (1 << 3) | (1 << 4) | (1 << 5) | (1 << 6),
|
||
ShardSizes: []int64{1000, 1000, 1000, 1000, 1000, 1000, 1000},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
nodeB := &master_pb.DataNodeInfo{
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"disk1": {
|
||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||
{
|
||
Id: 42,
|
||
Collection: "bucket-a",
|
||
// Shards 7..13 held here. Shard 9 (data) is the short tail.
|
||
EcIndexBits: (1 << 7) | (1 << 8) | (1 << 9) | (1 << 10) | (1 << 11) | (1 << 12) | (1 << 13),
|
||
ShardSizes: []int64{1000, 1000, 500, 1000, 1000, 1000, 1000},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
topo := &master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||
{
|
||
RackInfos: []*master_pb.RackInfo{
|
||
{
|
||
DataNodeInfos: []*master_pb.DataNodeInfo{nodeA, nodeB},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
stats := collectCollectionStats(topo)
|
||
|
||
got, ok := stats["bucket-a"]
|
||
if !ok {
|
||
t.Fatalf("expected collection bucket-a in stats, got: %v", stats)
|
||
}
|
||
|
||
const wantPhysical int64 = 13500
|
||
const wantLogical int64 = 9500
|
||
|
||
if got.PhysicalSize != wantPhysical {
|
||
t.Errorf("PhysicalSize: got %d, want %d", got.PhysicalSize, wantPhysical)
|
||
}
|
||
if got.LogicalSize != wantLogical {
|
||
t.Errorf("LogicalSize: got %d, want %d", got.LogicalSize, wantLogical)
|
||
}
|
||
}
|
||
|
||
// TestCollectCollectionStatsECEmptyCollection verifies that EC shards with
|
||
// an empty collection name are bucketed under "default".
|
||
func TestCollectCollectionStatsECEmptyCollection(t *testing.T) {
|
||
topo := &master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||
{
|
||
RackInfos: []*master_pb.RackInfo{
|
||
{
|
||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||
{
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"disk1": {
|
||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||
{
|
||
Id: 1,
|
||
Collection: "",
|
||
EcIndexBits: (1 << 0) | (1 << 10),
|
||
ShardSizes: []int64{2000, 2000},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
stats := collectCollectionStats(topo)
|
||
|
||
got, ok := stats["default"]
|
||
if !ok {
|
||
t.Fatalf("expected collection default in stats, got: %v", stats)
|
||
}
|
||
if got.PhysicalSize != 4000 {
|
||
t.Errorf("PhysicalSize: got %d, want 4000", got.PhysicalSize)
|
||
}
|
||
// Only shard 0 is a data shard; shard 10 is parity.
|
||
if got.LogicalSize != 2000 {
|
||
t.Errorf("LogicalSize: got %d, want 2000", got.LogicalSize)
|
||
}
|
||
}
|
||
|
||
// TestCollectCollectionStatsECFileAndDeleteCountAggregation verifies that
|
||
// FileCount for an EC volume is deduped across nodes (every shard holder has
|
||
// an identical .ecx, so the total entry count is taken once) while
|
||
// DeleteCount is summed (each needle delete tombstones exactly one node's
|
||
// .ecx, so the true delete total is the sum of every holder's local count).
|
||
func TestCollectCollectionStatsECFileAndDeleteCountAggregation(t *testing.T) {
|
||
// Volume id=7 reported by three nodes. Every node reports file_count=100
|
||
// (same .ecx). Local delete counts: 5 + 3 + 2 = 10 deletes total.
|
||
// Expected live object count = 100 - 10 = 90.
|
||
makeNode := func(bits uint32, sizes []int64, deleteCount uint64) *master_pb.DataNodeInfo {
|
||
return &master_pb.DataNodeInfo{
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"disk1": {
|
||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||
{
|
||
Id: 7,
|
||
Collection: "bucket-a",
|
||
EcIndexBits: bits,
|
||
ShardSizes: sizes,
|
||
FileCount: 100,
|
||
DeleteCount: deleteCount,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
topo := &master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||
{
|
||
RackInfos: []*master_pb.RackInfo{
|
||
{
|
||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||
makeNode((1<<0)|(1<<1)|(1<<2)|(1<<3), []int64{1000, 1000, 1000, 1000}, 5),
|
||
makeNode((1<<4)|(1<<5)|(1<<6)|(1<<7), []int64{1000, 1000, 1000, 1000}, 3),
|
||
makeNode((1<<8)|(1<<9)|(1<<10)|(1<<11)|(1<<12)|(1<<13), []int64{1000, 1000, 1000, 1000, 1000, 1000}, 2),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
stats := collectCollectionStats(topo)
|
||
|
||
got, ok := stats["bucket-a"]
|
||
if !ok {
|
||
t.Fatalf("expected collection bucket-a in stats, got: %v", stats)
|
||
}
|
||
if got.FileCount != 90 {
|
||
t.Errorf("FileCount: got %d, want 90 (100 total - 10 deletes summed)", got.FileCount)
|
||
}
|
||
// Sanity: 14 shards × 1000 bytes physical, 10 data shards logical.
|
||
if got.PhysicalSize != 14000 {
|
||
t.Errorf("PhysicalSize: got %d, want 14000", got.PhysicalSize)
|
||
}
|
||
if got.LogicalSize != 10000 {
|
||
t.Errorf("LogicalSize: got %d, want 10000", got.LogicalSize)
|
||
}
|
||
}
|
||
|
||
// TestCollectCollectionStatsECFileCountMaxDedupe verifies that EC file_count
|
||
// is taken as the max across reporting nodes rather than the first-seen
|
||
// value. A node that has not yet finished loading .ecx reports file_count=0,
|
||
// which previously poisoned the aggregate and rendered buckets backed by EC
|
||
// volumes as "0 objects".
|
||
func TestCollectCollectionStatsECFileCountMaxDedupe(t *testing.T) {
|
||
makeNode := func(bits uint32, sizes []int64, fileCount uint64) *master_pb.DataNodeInfo {
|
||
return &master_pb.DataNodeInfo{
|
||
DiskInfos: map[string]*master_pb.DiskInfo{
|
||
"disk1": {
|
||
EcShardInfos: []*master_pb.VolumeEcShardInformationMessage{
|
||
{
|
||
Id: 11,
|
||
Collection: "bucket-b",
|
||
EcIndexBits: bits,
|
||
ShardSizes: sizes,
|
||
FileCount: fileCount,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
topo := &master_pb.TopologyInfo{
|
||
DataCenterInfos: []*master_pb.DataCenterInfo{
|
||
{
|
||
RackInfos: []*master_pb.RackInfo{
|
||
{
|
||
// First-reporting node has a stale fileCount of 0,
|
||
// second node reports the authoritative 6.
|
||
DataNodeInfos: []*master_pb.DataNodeInfo{
|
||
makeNode((1<<0)|(1<<1)|(1<<2)|(1<<3)|(1<<4)|(1<<5)|(1<<6), []int64{1, 1, 1, 1, 1, 1, 1}, 0),
|
||
makeNode((1<<7)|(1<<8)|(1<<9)|(1<<10)|(1<<11)|(1<<12)|(1<<13), []int64{1, 1, 1, 1, 1, 1, 1}, 6),
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}
|
||
|
||
stats := collectCollectionStats(topo)
|
||
got, ok := stats["bucket-b"]
|
||
if !ok {
|
||
t.Fatalf("expected collection bucket-b in stats, got: %v", stats)
|
||
}
|
||
if got.FileCount != 6 {
|
||
t.Errorf("FileCount: got %d, want 6 (max across reporters)", got.FileCount)
|
||
}
|
||
}
|