From 6796ab6db1d7d70013111c31f05c174e1578f84c Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Wed, 13 May 2026 00:00:24 -0700 Subject: [PATCH] fix(s3/lifecycle): trust persisted cursor; never bump past pending events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drain freezes cursorAdvanceTo at the last pre-skip event so pending matches (DueTime > runNow) re-enter the subscription next pass. Combined with the new cursor persistence, the floor bump (runNow - maxTTL) then orphans the very events the drain stopped at. Concrete: a rule with TTL == maxTTL fires at runNow == PUT_TIME + maxTTL, so floor (= runNow - maxTTL) lands exactly on PUT_TIME. If the last advance saved a cursor right before the not-yet-due PUT (e.g., keep2/* between expire1/* and expire3/* on the same shard), the floor bump on pass 9 skips past the expire3 event itself — the worker never re-reads it. Test symptom: expire3/* never expires when worker shards include other earlier no-match events. Cold start (found=false) still subscribes from runNow - maxTTL. Steady state honors the cursor verbatim. --- weed/s3api/s3lifecycle/dailyrun/run.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/weed/s3api/s3lifecycle/dailyrun/run.go b/weed/s3api/s3lifecycle/dailyrun/run.go index ebf676279..82fed0624 100644 --- a/weed/s3api/s3lifecycle/dailyrun/run.go +++ b/weed/s3api/s3lifecycle/dailyrun/run.go @@ -272,11 +272,17 @@ func runShard(ctx context.Context, cfg Config, snap *engine.Snapshot, runNow tim } // Cold start: scan from now-maxTTL so already-due objects within - // meta-log retention still expire. + // meta-log retention still expire. In steady state honor the + // cursor as-is: the drain freezes the cursor at the last pre-skip + // event so pending matches with DueTime == TsNs+maxTTL stay in + // scope across passes. Bumping forward to runNow-maxTTL would + // orphan exactly those events (the test_lifecyclev2_expiration + // regression: cursor saved at the no-match event right before + // the not-yet-due expire3 PUT, then floor at runNow=PUT+maxTTL + // equals PUT — bumping past the expire3 event itself). startTsNs := persisted.TsNs - floor := runNow.Add(-maxTTL).UnixNano() - if startTsNs < floor { - startTsNs = floor + if !found { + startTsNs = runNow.Add(-maxTTL).UnixNano() } lastOK, _, drainErr := drainShardEvents(ctx, cfg, runNow, shardID, snap, startTsNs)