shell: warn in volume.list when a volume id spans collections (#9759)

* shell: warn in volume.list when a volume id spans collections

A reused volume id, the result of the master handing out an id already
used by another collection (for example after losing its max-volume-id
counter on restart), makes collection.delete destroy the wrong
collection's data and makes any bare-id lookup, move, or vacuum
ambiguous. volume.list now scans the full topology and warns on ids
present in more than one collection so the clash is visible before any
destructive operation.

* volume.list: track duplicate ids lazily, sort with slices.Sort

Allocate the per-id collection set only on the first cross-collection clash
instead of one set per volume, so allocations scale with duplicates rather
than the volume count.
This commit is contained in:
Chris Lu
2026-05-31 11:52:39 -07:00
committed by GitHub
parent 35ab67fa8a
commit fdfeb4063c
2 changed files with 169 additions and 0 deletions
+83
View File
@@ -132,9 +132,92 @@ func (c *commandVolumeList) writeTopologyInfo(writer io.Writer, t *master_pb.Top
s.add(c.writeDataCenterInfo(writer, dc, verbosityLevel))
}
output(verbosityLevel >= 0, writer, "%+v \n", s)
// Scan the full topology (ignoring any -collection/-volumeId/-dataCenter
// filters) so this cluster-health warning always fires.
writeDuplicateVolumeIdWarning(writer, findDuplicateVolumeIds(t))
return s
}
// findDuplicateVolumeIds returns volume ids that appear under more than one
// collection, mapped to the sorted list of collections they live in. Volume
// ids are meant to be globally unique; a duplicate means the master handed out
// an id already in use by another collection (historically after losing its
// max-volume-id counter on restart, see the master resumeState flag). Such ids
// are dangerous: collection.delete on one collection destroys that collection's
// copy, and any operation keyed on the bare id (lookup, move, vacuum) is
// ambiguous. Both normal volumes and EC shards are scanned. Replicas of the
// same volume share a collection, so they collapse into a single entry and do
// not register as duplicates.
func findDuplicateVolumeIds(t *master_pb.TopologyInfo) map[uint32][]string {
// Duplicates are rare, so track only the first collection seen per id and
// allocate a set lazily on the first clash. Keeps allocations O(duplicates)
// rather than O(volumes), which matters on clusters with millions of ids.
firstCollectionByVid := make(map[uint32]string)
collectionsByVid := make(map[uint32]map[string]struct{})
note := func(vid uint32, collection string) {
if collections := collectionsByVid[vid]; collections != nil {
collections[collection] = struct{}{}
return
}
first, seen := firstCollectionByVid[vid]
if !seen {
firstCollectionByVid[vid] = collection
return
}
if first == collection {
return
}
collectionsByVid[vid] = map[string]struct{}{first: {}, collection: {}}
}
for _, dc := range t.DataCenterInfos {
for _, rack := range dc.RackInfos {
for _, dn := range rack.DataNodeInfos {
for _, disk := range dn.DiskInfos {
for _, vi := range disk.VolumeInfos {
note(vi.Id, vi.Collection)
}
for _, ec := range disk.EcShardInfos {
note(ec.Id, ec.Collection)
}
}
}
}
}
duplicates := make(map[uint32][]string)
for vid, collections := range collectionsByVid {
names := make([]string, 0, len(collections))
for name := range collections {
names = append(names, name)
}
sort.Strings(names)
duplicates[vid] = names
}
return duplicates
}
func writeDuplicateVolumeIdWarning(writer io.Writer, duplicates map[uint32][]string) {
if len(duplicates) == 0 {
return
}
vids := make([]uint32, 0, len(duplicates))
for vid := range duplicates {
vids = append(vids, vid)
}
slices.Sort(vids)
fmt.Fprintf(writer, "\nWARNING: %d volume id(s) exist in more than one collection. "+
"Deleting one collection (e.g. collection.delete) will destroy that collection's data on these shared ids, "+
"and lookups/moves on the bare id are ambiguous. Verify before any destructive operation:\n", len(vids))
for _, vid := range vids {
collections := duplicates[vid]
for i, name := range collections {
if name == "" {
collections[i] = `"" (default)`
}
}
fmt.Fprintf(writer, " volume %d in collections: %s\n", vid, strings.Join(collections, ", "))
}
}
func (c *commandVolumeList) writeDataCenterInfo(writer io.Writer, t *master_pb.DataCenterInfo, verbosityLevel int) statistics {
var s statistics
slices.SortFunc(t.RackInfos, func(a, b *master_pb.RackInfo) int {