From e785f59d6fb266d4abbeebb58cd0b26e6f920afd Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sat, 9 May 2026 23:30:47 -0700 Subject: [PATCH] fix(s3/lifecycle): wire ExpirationDate dispatch through bootstrap walker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The walker explicitly skipped ModeScanAtDate actions on the comment "SCAN_AT_DATE runs its own date-triggered bootstrap" — but no such bootstrap exists in the scheduler or shell layer. The result: rules with Expiration{Date: ...} compiled correctly, populated the snapshot's dateActions map, and were never dispatched. ExpirationDate is silently a no-op in production. EvaluateAction already handles ActionKindExpirationDate correctly (rejects when now.Before(rule.ExpirationDate), otherwise emits ActionDeleteObject). The walker just needed to fall through instead of skipping. Pre-date walks become no-ops via EvaluateAction's date check; post-date walks expire eligible objects. Un-skip TestLifecycleExpirationDateInThePast — it now exercises the fixed path end-to-end. --- .../lifecycle/s3_lifecycle_expiration_date_test.go | 9 --------- weed/s3api/s3lifecycle/bootstrap/walker.go | 14 ++++++++++---- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go b/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go index 199a0bef4..a60efeb82 100644 --- a/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go +++ b/test/s3/lifecycle/s3_lifecycle_expiration_date_test.go @@ -20,15 +20,6 @@ import ( // separate compile + dispatch branch (engine.decideMode case // ActionKindExpirationDate) that wouldn't be exercised otherwise. func TestLifecycleExpirationDateInThePast(t *testing.T) { - // SCAN_AT_DATE is a documented mode in engine.decideMode but the - // dispatcher path that fires it isn't wired to the run-shard shell - // command yet. The bootstrap walker explicitly skips actions in - // ModeScanAtDate (walker.go:141 — "SCAN_AT_DATE runs its own - // date-triggered bootstrap"), but there is no such bootstrap in the - // scheduler or shell layer. Until that lands, this test would - // always time out. Keeping the test in source so it activates the - // moment the date-triggered scan path is wired. - t.Skip("ScanAtDate dispatch path not yet wired to run-shard; activate when the date-bootstrap lands") c := s3Client(t) fc, fcClose := filerClient(t) defer fcClose() diff --git a/weed/s3api/s3lifecycle/bootstrap/walker.go b/weed/s3api/s3lifecycle/bootstrap/walker.go index e9ee1a547..adedb4fcd 100644 --- a/weed/s3api/s3lifecycle/bootstrap/walker.go +++ b/weed/s3api/s3lifecycle/bootstrap/walker.go @@ -135,10 +135,16 @@ func walkEntry(ctx context.Context, snap *engine.Snapshot, bucket string, entry if action == nil { continue } - // SCAN_AT_DATE runs its own date-triggered bootstrap. DISABLED can - // be flipped at runtime independent of XML Status, so skip it even - // though EvaluateAction would also reject. - if action.Mode == engine.ModeScanAtDate || action.Mode == engine.ModeDisabled { + // DISABLED can be flipped at runtime independent of XML Status, + // so skip it even though EvaluateAction would also reject. + // SCAN_AT_DATE actions are processed here too — the date check + // in EvaluateAction (now.Before(rule.ExpirationDate)) gates the + // dispatch, so pre-date walks are no-ops and post-date walks + // expire eligible objects. The earlier "scan-at-date runs its + // own bootstrap" plan was never wired; until that lands, the + // regular bootstrap walk is the only path that fires + // ExpirationDate rules. + if action.Mode == engine.ModeDisabled { continue } // (kind, info) shape gate: ABORT_MPU only on MPU init records,