ec.balance: add a -volumeIds filter (#10667)

* ec.balance: add a -volumeIds filter

Collection scope is often too broad for maintenance. -volumeIds narrows the
plan to the given ec volume ids by leaving every other volume out of the
topology handed to the planner, so no phase, dedup included, can plan against
them. Ids with no ec shard in the selected collection, dataCenter and disk type
are rejected rather than silently skipped.

* ec.encode: key the orphan sweep without narrowing the volume id

int is 32-bit on 32-bit builds, so int(vid) wraps for volume ids above
MaxInt32. Format the id as the uint32 it is.
This commit is contained in:
Chris Lu
2026-08-09 09:37:49 -07:00
committed by GitHub
parent 567052bfb6
commit e5dc98dcb2
6 changed files with 177 additions and 50 deletions
+59
View File
@@ -1,6 +1,7 @@
package shell
import (
"strings"
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
@@ -458,3 +459,61 @@ func TestCommandEcBalanceIssue8793Topology(t *testing.T) {
}
}
}
// TestCommandEcBalanceVolumeIdsFilter checks that -volumeIds keeps every phase,
// dedup included, off the volumes that were not asked for.
func TestCommandEcBalanceVolumeIdsFilter(t *testing.T) {
ecb := &ecBalancer{
ecNodes: []*EcNode{
// Volume 1: all shards on one node, so balancing has plenty to move.
newEcNode("dc1", "rack1", "dn1", 100).addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}),
// Volume 2 is equally lopsided, and shard 0 is duplicated on dn3.
newEcNode("dc1", "rack2", "dn2", 100).addEcVolumeAndShardsForTest(2, "c1", []erasure_coding.ShardId{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}),
newEcNode("dc1", "rack3", "dn3", 100).addEcVolumeAndShardsForTest(2, "c1", []erasure_coding.ShardId{0}),
newEcNode("dc1", "rack4", "dn4", 100),
newEcNode("dc1", "rack5", "dn5", 100),
newEcNode("dc1", "rack6", "dn6", 100),
},
applyBalancing: false,
diskType: types.HardDriveType,
volumeIds: map[uint32]bool{1: true},
}
if err := ecb.balance([]string{"c1"}); err != nil {
t.Fatalf("balance: %v", err)
}
// Volume 1 spreads out.
if count := ecb.ecNodes[0].localShardIdCount(1); count == 14 {
t.Errorf("volume 1 was not balanced: dn1 still holds all %d shards", count)
}
// Volume 2 keeps every shard where it was, duplicate included.
if count := ecb.ecNodes[1].localShardIdCount(2); count != 14 {
t.Errorf("dn2 holds %d shards of the unselected volume 2, want 14", count)
}
if count := ecb.ecNodes[2].localShardIdCount(2); count != 1 {
t.Errorf("dn3 holds %d shards of the unselected volume 2, want the duplicate to survive", count)
}
}
func TestCommandEcBalanceVolumeIdsNotFound(t *testing.T) {
ecb := &ecBalancer{
ecNodes: []*EcNode{
newEcNode("dc1", "rack1", "dn1", 100).addEcVolumeAndShardsForTest(1, "c1", []erasure_coding.ShardId{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}),
newEcNode("dc1", "rack2", "dn2", 100),
},
applyBalancing: false,
diskType: types.HardDriveType,
volumeIds: map[uint32]bool{1: true, 99: true},
}
err := ecb.balance([]string{"c1"})
if err == nil || !strings.Contains(err.Error(), "[99]") {
t.Fatalf("want an error naming volume 99, got %v", err)
}
// The valid id must not be balanced either: the plan is all-or-nothing.
if count := ecb.ecNodes[0].localShardIdCount(1); count != 14 {
t.Errorf("dn1 holds %d shards, want the rejected plan to leave volume 1 untouched", count)
}
}