From 7ce7fc8d1e206a43bbc7074079efd4abb698d2df Mon Sep 17 00:00:00 2001 From: Denis Babchuk Date: Thu, 29 Oct 2026 22:09:03 +0300 Subject: [PATCH] fix(pools): do not list or move free versions as delete markers Signed-off-by: Denis Babchuk --- cmd/data-scanner.go | 2 +- cmd/erasure-server-pool-decom.go | 46 +++- cmd/erasure-server-pool-free-version_test.go | 218 +++++++++++++++++++ cmd/erasure-server-pool.go | 2 +- cmd/global-heal.go | 2 +- cmd/metacache-entries.go | 16 +- cmd/metacache-set.go | 5 + cmd/xl-storage-format-utils.go | 4 +- 8 files changed, 288 insertions(+), 7 deletions(-) diff --git a/cmd/data-scanner.go b/cmd/data-scanner.go index 0475c4a53..13d0b99f0 100644 --- a/cmd/data-scanner.go +++ b/cmd/data-scanner.go @@ -776,7 +776,7 @@ func (f *folderScanner) scanFolder(ctx context.Context, folder cachedFolder, int } // We got an entry which we should be able to heal. - fiv, err := entry.fileInfoVersions(bucket) + fiv, err := entry.allFileInfoVersions(bucket) if err != nil { err := bgSeq.queueHealTask(healSource{ bucket: bucket, diff --git a/cmd/erasure-server-pool-decom.go b/cmd/erasure-server-pool-decom.go index 3c21825bb..e0064bfcd 100644 --- a/cmd/erasure-server-pool-decom.go +++ b/cmd/erasure-server-pool-decom.go @@ -982,6 +982,9 @@ func (z *erasureServerPools) decommissionPool(ctx context.Context, idx int, pool if decommissioned == len(fivs.Versions) { stopFn := globalDecommissionMetrics.log(decomMetricDecommissionRemoveObject, idx, bi.Name, entry.name) err := z.removeMovedVersions(ctx, bi.Name, entry.name, idx, fivs.Versions) + if err == nil { + err = z.removeFreeVersions(ctx, bi.Name, entry.name, idx, fivs.FreeVersions) + } stopFn(0, err) auditLogDecom(ctx, "DecomDeleteObject", bi.Name, entry.name, "", err) if err != nil { @@ -1108,6 +1111,45 @@ func (z *erasureServerPools) decommissionInBackground(ctx context.Context, idx i return nil } +// removeFreeVersions removes the free versions fvs of object from the pool +// idx being decommissioned, as the scanner removes free versions: first the +// remote object of each, then the free version. A free version is not moved. +// In another pool it would be a delete marker on top of the object, and once +// this pool is gone nothing would remove its remote object. A remote object +// that is already gone is not an error. +func (z *erasureServerPools) removeFreeVersions(ctx context.Context, bucket, object string, idx int, fvs []FileInfo) error { + if len(fvs) == 0 { + return nil + } + for _, fv := range fvs { + err := deleteObjectFromRemoteTier(ctx, fv.TransitionedObjName, fv.TransitionVersionID, fv.TransitionTier) + if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { + return fmt.Errorf("unable to remove the remote object %s of a free version of %s/%s from tier %s: %w", + fv.TransitionedObjName, bucket, object, fv.TransitionTier, err) + } + } + object = encodeDirObject(object) + lk := z.NewNSLock(bucket, object) + lkctx, err := lk.GetLock(ctx, globalOperationTimeout) + if err != nil { + return err + } + ctx = lkctx.Context() + defer lk.Unlock(lkctx) + for _, fv := range fvs { + _, err := z.serverPools[idx].DeleteObject(ctx, bucket, object, ObjectOptions{ + VersionID: fv.VersionID, + InclFreeVersions: true, + NoLock: true, + NoAuditLog: true, + }) + if err != nil && !isErrObjectNotFound(err) && !isErrVersionNotFound(err) { + return err + } + } + return nil +} + func (z *erasureServerPools) checkAfterDecom(ctx context.Context, idx int) error { buckets, err := z.getBucketsToDecommission(ctx) if err != nil { @@ -1192,7 +1234,9 @@ func (z *erasureServerPools) checkAfterDecom(ctx context.Context, idx int) error } } - versionsFound += len(fivs.Versions) - ignored + // A free version left behind keeps a remote object that + // nothing removes once the pool is gone. + versionsFound += len(fivs.Versions) - ignored + len(fivs.FreeVersions) }); err != nil { return err } diff --git a/cmd/erasure-server-pool-free-version_test.go b/cmd/erasure-server-pool-free-version_test.go index c4177906a..507e56f3a 100644 --- a/cmd/erasure-server-pool-free-version_test.go +++ b/cmd/erasure-server-pool-free-version_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + madmin "github.com/minio/madmin-go/v3" "github.com/minio/minio/internal/bucket/lifecycle" objectlock "github.com/minio/minio/internal/bucket/object/lock" ) @@ -183,6 +184,29 @@ func (f *freeVersionTest) stored(pool int, object string) []string { return got } +// versions labels the versions of a listing as "