fix(ilm): recheck read expiry for the resolved version

Recheck only local ordinary-delete candidates that need an all-pool lifecycle decision. Match UUID/null identity to the version actually read and queue its global metadata and event; retain a successful read on recheck failure, absence, or refusal.

Keep GET reader cleanup, authorization, proxy reads, single-pool reads, restored-copy cleanup, and expiry response headers on their existing paths. Cover signed GET/HEAD, queue contents, metadata faults, missing matches, no-scan boundaries, and reader-lock release.

Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
Feng Ruohang
2026-11-03 21:07:55 +08:00
parent 3a1e132f16
commit 65c59dd860
3 changed files with 421 additions and 2 deletions
+37
View File
@@ -251,6 +251,43 @@ func (z *erasureServerPools) expiryNeedsAllPools(opts ObjectOptions) bool {
return opts.DeletePrefix || opts.VersionID != "" || opts.Versioned || opts.VersionSuspended
}
// recheckReadExpiry only rechecks a local ordinary-delete candidate. The
// global latest expiring does not authorize hiding a different version that
// GET/HEAD has already read. On an incomplete or rejected recheck, keep that
// successful read and do not queue an expiry. Leave response metadata intact.
func recheckReadExpiry(ctx context.Context, objectAPI ObjectLayer, oi ObjectInfo, evt lifecycle.Event) (ObjectInfo, lifecycle.Event) {
z, ok := objectAPI.(*erasureServerPools)
if !ok || !evt.Action.Delete() || evt.Action.DeleteRestored() || !z.expiryNeedsAllPools(ObjectOptions{
Expiration: ExpirationOptions{Expire: true},
VersionID: oi.VersionID,
Versioned: globalBucketVersioningSys.PrefixEnabled(oi.Bucket, oi.Name),
VersionSuspended: globalBucketVersioningSys.PrefixSuspended(oi.Bucket, oi.Name),
DeletePrefix: evt.Action.DeleteAll(),
}) {
return oi, evt
}
versions, events, err := z.lifecycleEventsAllPools(ctx, oi.Bucket, encodeDirObject(oi.Name))
if err == nil {
versionID := oi.VersionID
if versionID == "" {
versionID = nullVersionID
}
for i, version := range versions {
id := version.VersionID
if id == "" {
id = nullVersionID
}
if id == versionID {
if events[i].Action.Delete() && !events[i].Action.DeleteRestored() {
return version, events[i]
}
break
}
}
}
return oi, lifecycle.Event{Action: lifecycle.NoneAction}
}
// migrationExpiryApplies reports whether the ILM expiry evt, which a
// decommission or rebalance decided for oi on the versions of the pool it
// walks, applies with the versions of the object in all pools too. Only then