fix(heal): send the dry-run flag to remote bucket heal

remotePeerS3Client.HealBucket sent only the bucket name and the remove
flag, and HealBucketHandler built its heal options from those two
values. During a dry-run bucket heal every remote node therefore ran a
real one: it created the bucket directory on drives that lacked it, or
removed empty bucket directories when the bucket was missing from
every pool. Only the coordinating node honored DryRun.

Add a "dry-run" key to the peer message and read it into
HealOpts.DryRun on the receiving side, so healBucketLocal returns
before MakeVol and DeleteVol on every node. The message is a string
map: an older node ignores the key, and a missing key reads as false,
so a mixed-version cluster behaves as before until every node is
upgraded.

The regression calls the real client over a test grid connection to
the real handler. It fails on main, and it fails if either side of the
change is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-05 01:21:13 +08:00
parent c8dd5678bb
commit dd0a2da55e
4 changed files with 121 additions and 4 deletions
+11 -4
View File
@@ -354,10 +354,7 @@ func (client *remotePeerS3Client) HealBucket(ctx context.Context, bucket string,
return madmin.HealResultItem{}, nil
}
mss := grid.NewMSSWith(map[string]string{
peerS3Bucket: bucket,
peerS3BucketDeleted: strconv.FormatBool(opts.Remove),
})
mss := healBucketPeerMSS(bucket, opts)
ctx, cancel := context.WithTimeout(ctx, globalDriveConfig.GetMaxTimeout())
defer cancel()
@@ -366,6 +363,16 @@ func (client *remotePeerS3Client) HealBucket(ctx context.Context, bucket string,
return resp.ValueOrZero(), toStorageErr(err)
}
// healBucketPeerMSS encodes the bucket heal options a peer applies to its
// local drives. Peers that predate the dry-run key ignore it.
func healBucketPeerMSS(bucket string, opts madmin.HealOpts) *grid.MSS {
return grid.NewMSSWith(map[string]string{
peerS3Bucket: bucket,
peerS3BucketDeleted: strconv.FormatBool(opts.Remove),
peerS3BucketDryRun: strconv.FormatBool(opts.DryRun),
})
}
// GetBucketInfo returns bucket stat info from a peer
func (client *remotePeerS3Client) GetBucketInfo(ctx context.Context, bucket string, opts BucketOptions) (BucketInfo, error) {
conn := client.gridConn()