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
+3 -30
View File
@@ -163,7 +163,7 @@ func (c *commandEcEncode) Do(args []string, commandEnv *CommandEnv, writer io.Wr
if *volumeId != 0 {
volumeIds = append(volumeIds, needle.VolumeId(*volumeId))
} else {
volumeIds, err = parseEcEncodeVolumeIds(*volumeIdsStr)
volumeIds, err = parseVolumeIdsFlag(*volumeIdsStr)
if err != nil {
return err
}
@@ -202,33 +202,6 @@ func (c *commandEcEncode) Do(args []string, commandEnv *CommandEnv, writer io.Wr
return nil
}
func parseEcEncodeVolumeIds(volumeIdsStr string) ([]needle.VolumeId, error) {
var volumeIds []needle.VolumeId
seen := make(map[needle.VolumeId]bool)
for _, part := range strings.Split(volumeIdsStr, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
vidValue, err := strconv.ParseUint(part, 10, 32)
if err != nil || vidValue == 0 {
return nil, fmt.Errorf("invalid volume id %q in -volumeIds", part)
}
// ParseUint with bitSize 32 bounds the value; convert through uint32
// (matching the rest of the codebase) so the narrowing is provably safe.
vid := needle.VolumeId(uint32(vidValue))
if seen[vid] {
continue
}
seen[vid] = true
volumeIds = append(volumeIds, vid)
}
if len(volumeIds) == 0 {
return nil, fmt.Errorf("-volumeIds does not contain any valid volume id")
}
return volumeIds, nil
}
func chunkVolumeIds(volumeIds []needle.VolumeId, batchSize int) [][]needle.VolumeId {
if batchSize <= 0 || len(volumeIds) == 0 {
return [][]needle.VolumeId{volumeIds}
@@ -288,7 +261,7 @@ func processEcEncodeBatch(commandEnv *CommandEnv, writer io.Writer, volumeIds []
// safely verified and deleted without waiting for all batches to finish.
// skippedNodes are excluded so a recovered node's stale orphan is never
// paired with a new-generation shard.
if err := EcBalance(commandEnv, balanceCollections, "", rp, diskType, maxParallelization, applyBalancing, skippedNodes); err != nil {
if err := EcBalance(commandEnv, balanceCollections, "", rp, diskType, maxParallelization, applyBalancing, skippedNodes, nil); err != nil {
return fmt.Errorf("re-balance ec shards for collection(s) %v: %w", balanceCollections, err)
}
if err := verifyEcShardsBeforeDelete(commandEnv, volumeIds, diskType, applyBalancing); err != nil {
@@ -518,7 +491,7 @@ func clearPreexistingEcShards(commandEnv *CommandEnv, topologyInfo *master_pb.To
// it. Always delete the full shard-id range so a wider custom ratio's
// leftovers are covered too.
reportedKey := func(addr pb.ServerAddress, vid uint32) string {
return string(addr) + "\x00" + strconv.Itoa(int(vid))
return string(addr) + "\x00" + strconv.FormatUint(uint64(vid), 10)
}
reported := make(map[string]struct{})
var nodes []pb.ServerAddress