From 82648cca53a1ed6178dbb687c1c07c0f9cfe8232 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Sun, 10 May 2026 10:36:54 -0700 Subject: [PATCH] test(s3/lifecycle/engine): pin delay-group dedup across buckets (#9418) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compile a 100-bucket × 5-rule snapshot where the five Days values include duplicates (1, 1, 7, 7, 30) and assert: - snap.actions has 500 entries — every (bucket, rule) compiles to its own ActionKey, no collapse. - snap.originalDelayGroups has exactly 3 entries — the routing index is keyed by Delay, so same-day rules across all buckets share a group. This is the property that lets the dispatcher index by delay group rather than per-rule. - Per-group key count = (rules with that day) × buckets, so every action is reachable from its group entry. --- weed/s3api/s3lifecycle/engine/engine_test.go | 58 ++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/weed/s3api/s3lifecycle/engine/engine_test.go b/weed/s3api/s3lifecycle/engine/engine_test.go index e61949f1e..829c7c915 100644 --- a/weed/s3api/s3lifecycle/engine/engine_test.go +++ b/weed/s3api/s3lifecycle/engine/engine_test.go @@ -1,6 +1,7 @@ package engine import ( + "strconv" "testing" "time" @@ -322,3 +323,60 @@ func TestEngine_SnapshotAtomicSwap(t *testing.T) { t.Fatalf("Engine.Snapshot should return the latest") } } + +// TestCompile_DelayGroupsDedupeAcrossBuckets pins the scaling property +// the dispatcher relies on: originalDelayGroups is keyed by Delay, so N +// rules sharing the same Days threshold across M buckets collapse into +// one delay group with N*M action keys — not N*M groups. The dispatch +// path can then index by delay group rather than per-rule. +func TestCompile_DelayGroupsDedupeAcrossBuckets(t *testing.T) { + const buckets = 100 + const rulesPerBucket = 5 + // Five rules per bucket, but only three distinct day values, so two + // pairs of rules per bucket share a delay group. + dayValues := []int{1, 1, 7, 7, 30} + distinctDays := map[int]bool{} + for _, d := range dayValues { + distinctDays[d] = true + } + + inputs := make([]CompileInput, buckets) + prior := map[s3lifecycle.ActionKey]PriorState{} + for b := 0; b < buckets; b++ { + bucket := "b" + strconv.Itoa(b) + rules := make([]*s3lifecycle.Rule, rulesPerBucket) + for r, days := range dayValues { + rule := ruleExpDays("r"+strconv.Itoa(r), "p"+strconv.Itoa(r)+"/", days) + rules[r] = rule + prior[s3lifecycle.ActionKey{ + Bucket: bucket, + RuleHash: s3lifecycle.RuleHash(rule), + ActionKind: s3lifecycle.ActionKindExpirationDays, + }] = PriorState{BootstrapComplete: true} + } + inputs[b] = CompileInput{Bucket: bucket, Rules: rules} + } + + e := New() + snap := e.Compile(inputs, CompileOptions{PriorStates: prior}) + + if got := len(snap.actions); got != buckets*rulesPerBucket { + t.Fatalf("want %d actions (no collapse), got %d", buckets*rulesPerBucket, got) + } + if got := len(snap.originalDelayGroups); got != len(distinctDays) { + t.Fatalf("want %d delay groups (one per distinct Days), got %d", + len(distinctDays), got) + } + // Per-group key count: each distinct day appears in `count` rules per + // bucket, so the group holds count*buckets keys. + wantPerGroup := map[time.Duration]int{} + for _, d := range dayValues { + wantPerGroup[s3lifecycle.DaysToDuration(d)] += buckets + } + for delay, want := range wantPerGroup { + if got := len(snap.originalDelayGroups[delay]); got != want { + t.Fatalf("delay %v: want %d action keys, got %d", delay, want, got) + } + } +} +