From 24e664d6518f7fe667a22ce9695878c52fe324ab Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 27 May 2026 17:49:35 -0700 Subject: [PATCH] fix(shell): don't halt volume.fsck purge on a stuck read-only volume (#9714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(shell): don't halt volume.fsck purge on a stuck read-only volume A failed VolumeMarkWritable on one volume aborted the entire fsck purge run; per-volume errors now log and continue so remaining volumes still get purged. * fix(shell): unify volume.fsck per-volume skip logging at the caller Return the mark-writable error from purgeOneVolume instead of logging in two places — the caller already prints "skip purging volume N: %v" and defers still fire on the error return. * fix(shell): collect volume.fsck purge-skipped volumes and report at end Track volume IDs whose purge was skipped (mark-writable failure or other per-volume errors) and print a sorted summary so operators don't have to scrape the run log to find them. Deletes for those volumes are already skipped; this just makes them explicit. --- weed/shell/command_volume_fsck.go | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/weed/shell/command_volume_fsck.go b/weed/shell/command_volume_fsck.go index c2aa54d82..3b85f8610 100644 --- a/weed/shell/command_volume_fsck.go +++ b/weed/shell/command_volume_fsck.go @@ -448,6 +448,7 @@ func (c *commandVolumeFsck) findExtraChunksInVolumeServers(dataNodeVolumeIdToVIn // MasterClient.GetLocations, so iterating per replica here (as the old // code did) would issue N*N delete RPCs for N replicas. if applyPurging { + var skippedVolumeIds []uint32 for volumeId, orphanReplicaFileIds := range volumeIdOrphanFileIds { if len(orphanReplicaFileIds) == 0 { continue @@ -458,11 +459,19 @@ func (c *commandVolumeFsck) findExtraChunksInVolumeServers(dataNodeVolumeIdToVIn } // Call out to a closure per volume so the deferred "mark // readonly again" fires between volumes instead of piling up - // until findExtraChunksInVolumeServers returns. + // until findExtraChunksInVolumeServers returns. Per-volume + // failures (e.g. a replica stuck read-only) don't halt the + // rest of the run; the volume is remembered and its deletes + // are skipped. if err := c.purgeOneVolume(volumeId, orphanReplicaFileIds, volumeReplicaCounts[volumeId], readOnlyServerReplicas[volumeId]); err != nil { - return err + fmt.Fprintf(c.writer, "skip purging volume %d: %v\n", volumeId, err) + skippedVolumeIds = append(skippedVolumeIds, volumeId) } } + if len(skippedVolumeIds) > 0 { + sort.Slice(skippedVolumeIds, func(i, j int) bool { return skippedVolumeIds[i] < skippedVolumeIds[j] }) + fmt.Fprintf(c.writer, "skipped purge on %d volume(s): %v\n", len(skippedVolumeIds), skippedVolumeIds) + } } if !applyPurging { @@ -510,7 +519,8 @@ func (c *commandVolumeFsck) purgeOneVolume(volumeId uint32, orphanReplicaFileIds needleVID := needle.VolumeId(volumeId) for _, server := range readOnlyReplicas { if err := markVolumeWritable(c.env.option.GrpcDialOption, needleVID, server, true, false); err != nil { - return fmt.Errorf("mark volume %d on %v read/write: %v", volumeId, server, err) + // Replicas flipped writable earlier roll back via the defer. + return fmt.Errorf("mark %v writable: %v", server, err) } fmt.Fprintf(c.writer, "temporarily marked %d on server %v writable for forced purge\n", volumeId, server) defer markVolumeWritable(c.env.option.GrpcDialOption, needleVID, server, false, false)