From 7c542128c767b19327ca50cb03248b9474067ecb Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sun, 7 Jun 2026 22:42:51 -0700 Subject: [PATCH] vacuum: compact a read-only volume when an explicit volumeId is given (#9861) * vacuum: compact a read-only volume when an explicit volumeId is given The on-demand path no longer skips read-only volumes, so an operator can reclaim a benignly read-only (full/oversized) volume without marking it writable first. The background scan and all-volumes sweep still skip read-only, where the flag usually signals an unhealthy disk. * vacuum: copy locationList under lock for on-demand vacuum The volumeId>0 path passed the live vid2location entry into the async vacuum, where heartbeat-driven Register/UnRegister can mutate the slice concurrently. Snapshot it under accessLock, matching the sweep path. --- weed/topology/topology_vacuum.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/weed/topology/topology_vacuum.go b/weed/topology/topology_vacuum.go index a5ea14d05..d1bdf9080 100644 --- a/weed/topology/topology_vacuum.go +++ b/weed/topology/topology_vacuum.go @@ -240,9 +240,12 @@ func (t *Topology) Vacuum(grpcDialOption grpc.DialOption, garbageThreshold float vid := needle.VolumeId(volumeId) volumeLayout.accessLock.RLock() locationList, ok := volumeLayout.vid2location[vid] + if ok { + locationList = locationList.Copy() + } volumeLayout.accessLock.RUnlock() if ok { - t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate) + t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate, false) } } else { t.vacuumOneVolumeLayout(grpcDialOption, volumeLayout, c, garbageThreshold, maxParallelVacuumPerServer, preallocate, automatic) @@ -311,7 +314,7 @@ func (t *Topology) vacuumOneVolumeLayout(grpcDialOption grpc.DialOption, volumeL wg.Add(1) executor.Execute(func() { defer wg.Done() - t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate) + t.vacuumOneVolumeId(grpcDialOption, volumeLayout, c, garbageThreshold, locationList, vid, preallocate, true) // credit the quota for _, dn := range locationList.list { limiterLock.Lock() @@ -336,14 +339,20 @@ func (t *Topology) vacuumOneVolumeLayout(grpcDialOption grpc.DialOption, volumeL } -func (t *Topology) vacuumOneVolumeId(grpcDialOption grpc.DialOption, volumeLayout *VolumeLayout, c *Collection, garbageThreshold float64, locationList *VolumeLocationList, vid needle.VolumeId, preallocate int64) { +// skipReadOnly is set by the background scan and all-volumes sweep, where a +// read-only flag usually means an unhealthy disk. An explicit volumeId clears +// it so a benignly read-only (full/oversized) volume can be reclaimed. +func (t *Topology) vacuumOneVolumeId(grpcDialOption grpc.DialOption, volumeLayout *VolumeLayout, c *Collection, garbageThreshold float64, locationList *VolumeLocationList, vid needle.VolumeId, preallocate int64, skipReadOnly bool) { volumeLayout.accessLock.RLock() isReadOnly := volumeLayout.readonlyVolumes.IsTrue(vid) isEnoughCopies := volumeLayout.enoughCopies(vid) volumeLayout.accessLock.RUnlock() if isReadOnly { - return + if skipReadOnly { + return + } + glog.V(0).Infof("vacuuming read-only volume %d on explicit request", vid) } if !isEnoughCopies { glog.Warningf("skip vacuuming: not enough copies for volume:%d", vid)