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.
This commit is contained in:
Chris Lu
2026-05-12 21:13:06 -07:00
parent 0663a41bd5
commit 79a0c8541f
+14
View File
@@ -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,