From 79a0c8541f01db1b2773ff9849f5f538f9a31922 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Tue, 12 May 2026 21:13:06 -0700 Subject: [PATCH] fix(s3/lifecycle): walker fires for walker-only buckets (empty replay path) runShard's empty-replay sentinel (rsh == [32]byte{}) was returning BEFORE the steady-state walker check. A bucket whose only lifecycle rule was walker-only (ExpirationDate / ExpiredDeleteMarker / NewerNoncurrent) would never have it dispatched because: - ReplayContentHash only hashes replay-eligible kinds, so walker-only-only snapshots produce rsh == empty. - The early-return persisted the empty cursor and exited before the steady-state walker block at the bottom of the function. Move the walker invocation INTO the empty-replay branch so walker- only rules dispatch on the same path as mixed-rule buckets. TestLifecycleExpirationDateInThePast and TestLifecycleExpiredDeleteMarkerCleanup were both timing out their "object must be deleted" Eventually polls because of this. Caught on PR #9471's S3 Lifecycle Tests run after PR #9475 restored the shell entry point that exercises the integration tests. --- weed/s3api/s3lifecycle/dailyrun/run.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/weed/s3api/s3lifecycle/dailyrun/run.go b/weed/s3api/s3lifecycle/dailyrun/run.go index 2ac02ecb6..8c6366869 100644 --- a/weed/s3api/s3lifecycle/dailyrun/run.go +++ b/weed/s3api/s3lifecycle/dailyrun/run.go @@ -204,6 +204,20 @@ func runShard(ctx context.Context, cfg Config, snap *engine.Snapshot, runNow tim promoted := engine.PromotedHash(snap, retentionWindow) if rsh == [32]byte{} { + // No replay-eligible rules. Walker-only rules + // (ExpirationDate / ExpiredDeleteMarker / NewerNoncurrent) + // or scan_only-promoted rules might still need a walk; run + // the steady-state walker before persisting the empty + // cursor and returning. Without this, a bucket whose only + // rule is walker-bound would never have it dispatched — + // the bug TestLifecycleExpirationDateInThePast caught. + if cfg.Walker != nil { + if _, walkView := snap.RulesForShard(shardID, retentionWindow); walkView != nil && len(walkView.AllActions()) > 0 { + if werr := cfg.Walker(ctx, walkView, shardID); werr != nil { + return fmt.Errorf("shard=%d: steady walk (empty replay): %w", shardID, werr) + } + } + } return cfg.Persister.Save(ctx, shardID, Cursor{ TsNs: 0, RuleSetHash: rsh,