snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-16 03:23:42 +05:00
commit 89f5be005d
3997 changed files with 961970 additions and 0 deletions

View File

@@ -0,0 +1,381 @@
package engine
import (
"strconv"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
)
func ruleExpDays(id, prefix string, days int) *s3lifecycle.Rule {
return &s3lifecycle.Rule{
ID: id,
Status: s3lifecycle.StatusEnabled,
Prefix: prefix,
ExpirationDays: days,
}
}
func TestCompile_SingleRuleSingleAction(t *testing.T) {
e := New()
rule := ruleExpDays("r1", "logs/", 30)
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{
PriorStates: map[s3lifecycle.ActionKey]PriorState{
{Bucket: "b1", RuleHash: s3lifecycle.RuleHash(rule), ActionKind: s3lifecycle.ActionKindExpirationDays}: {
BootstrapComplete: true,
},
},
})
if got := len(snap.actions); got != 1 {
t.Fatalf("want 1 action, got %d", got)
}
for _, a := range snap.actions {
if !a.IsActive() {
t.Fatalf("bootstrap_complete + EVENT_DRIVEN should activate, got mode=%v", a.Mode)
}
if a.Mode != ModeEventDriven {
t.Fatalf("want EVENT_DRIVEN, got %v", a.Mode)
}
if a.Delay != s3lifecycle.DaysToDuration(30) {
t.Fatalf("want 30d delay, got %v", a.Delay)
}
}
}
func TestCompile_MultiAction_SiblingsHaveOwnEntries(t *testing.T) {
// One XML rule with three actions -> three CompiledActions, three
// distinct ActionKeys, three independent delay group memberships.
rule := &s3lifecycle.Rule{
ID: "multi",
Status: s3lifecycle.StatusEnabled,
Prefix: "data/",
ExpirationDays: 90,
NoncurrentVersionExpirationDays: 30,
AbortMPUDaysAfterInitiation: 7,
}
rh := s3lifecycle.RuleHash(rule)
prior := map[s3lifecycle.ActionKey]PriorState{}
for _, k := range s3lifecycle.RuleActionKinds(rule) {
prior[s3lifecycle.ActionKey{Bucket: "b1", RuleHash: rh, ActionKind: k}] = PriorState{BootstrapComplete: true}
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: prior})
if got := len(snap.actions); got != 3 {
t.Fatalf("want 3 actions, got %d", got)
}
wantDelays := map[time.Duration]bool{
s3lifecycle.DaysToDuration(90): true, // ExpirationDays
s3lifecycle.DaysToDuration(30): true, // NoncurrentDays
s3lifecycle.DaysToDuration(7): true, // AbortMPU
}
for delay, keys := range snap.originalDelayGroups {
if !wantDelays[delay] {
t.Fatalf("unexpected delay group %v", delay)
}
if len(keys) != 1 {
t.Fatalf("delay %v should hold exactly its own action, got %d", delay, len(keys))
}
}
if len(snap.originalDelayGroups) != 3 {
t.Fatalf("want 3 delay groups, got %d", len(snap.originalDelayGroups))
}
}
func TestCompile_BootstrapPendingIndexedButInactive(t *testing.T) {
// Without prior bootstrap_complete=true the action is pending_bootstrap.
// It IS indexed in originalDelayGroups (so a later MarkActive flip is
// routable without recompile), but IsActive() reads false so the
// reader's IsActive-filter skips dispatch.
rule := ruleExpDays("r1", "logs/", 30)
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{})
for _, a := range snap.actions {
if a.IsActive() {
t.Fatalf("pending_bootstrap action should not be active")
}
}
if len(snap.originalDelayGroups) != 1 {
t.Fatalf("EVENT_DRIVEN action should be indexed even when inactive, got %v", snap.originalDelayGroups)
}
}
func TestCompile_RetentionGate(t *testing.T) {
// Retention gate compares EventLogHorizon (scales with the day unit)
// against MetaLogRetention - BootstrapLookbackMin (where lookback is a
// fixed wall-clock minute count). Under the s3tests build tag the day
// unit shrinks to 10s, which collapses the margin to zero or negative
// and the gate degrades every rule. Skip there; the prod build still
// covers the gate semantics this test cares about.
if s3lifecycle.DaysToDuration(1) < time.Hour {
t.Skip("retention gate semantics require day-unit > lookback margin")
}
// A 90d ExpirationDays rule with 30d retention should land in scan_only.
// A 7d rule (under retention - lookback) stays event_driven.
long := ruleExpDays("long", "x/", 90)
short := ruleExpDays("short", "y/", 1)
rules := []*s3lifecycle.Rule{long, short}
prior := map[s3lifecycle.ActionKey]PriorState{}
for _, r := range rules {
k := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: s3lifecycle.RuleHash(r), ActionKind: s3lifecycle.ActionKindExpirationDays}
prior[k] = PriorState{BootstrapComplete: true}
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: rules}}, CompileOptions{
MetaLogRetention: s3lifecycle.DaysToDuration(30),
BootstrapLookbackMin: 5 * time.Minute,
PriorStates: prior,
})
for _, a := range snap.actions {
if a.Rule.ID == "long" && a.Mode != ModeScanOnly {
t.Fatalf("90d rule under 30d retention should be scan_only, got %v", a.Mode)
}
if a.Rule.ID == "short" && a.Mode != ModeEventDriven {
t.Fatalf("1d rule should be event_driven, got %v", a.Mode)
}
}
}
func TestCompile_RetentionUnboundedNeverGates(t *testing.T) {
// MetaLogRetention=0 means unbounded (default deployment); even a 100y
// rule should stay event_driven.
rule := ruleExpDays("decade", "x/", 36500)
prior := map[s3lifecycle.ActionKey]PriorState{
{RuleHash: s3lifecycle.RuleHash(rule), ActionKind: s3lifecycle.ActionKindExpirationDays}: {BootstrapComplete: true},
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: prior})
for _, a := range snap.actions {
if a.Mode != ModeEventDriven {
t.Fatalf("unbounded retention should keep event_driven, got %v", a.Mode)
}
}
}
func TestCompile_SiblingsDegradeIndependently(t *testing.T) {
// See TestCompile_RetentionGate for why the s3tests build can't honor
// the gate's absolute-time math.
if s3lifecycle.DaysToDuration(1) < time.Hour {
t.Skip("retention gate semantics require day-unit > lookback margin")
}
// 90d ExpirationDays + 7d AbortMPU under 30d retention: ExpirationDays
// degrades to scan_only, AbortMPU stays event_driven. The whole point
// of per-action keying.
rule := &s3lifecycle.Rule{
ID: "mixed",
Status: s3lifecycle.StatusEnabled,
Prefix: "x/",
ExpirationDays: 90,
AbortMPUDaysAfterInitiation: 7,
}
rh := s3lifecycle.RuleHash(rule)
prior := map[s3lifecycle.ActionKey]PriorState{
{RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}: {BootstrapComplete: true},
{RuleHash: rh, ActionKind: s3lifecycle.ActionKindAbortMPU}: {BootstrapComplete: true},
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{
MetaLogRetention: s3lifecycle.DaysToDuration(30),
BootstrapLookbackMin: 5 * time.Minute,
PriorStates: prior,
})
expDaysKey := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}
mpuKey := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: rh, ActionKind: s3lifecycle.ActionKindAbortMPU}
if snap.actions[expDaysKey].Mode != ModeScanOnly {
t.Fatalf("ExpirationDays should be scan_only under 30d retention")
}
if snap.actions[mpuKey].Mode != ModeEventDriven {
t.Fatalf("AbortMPU should stay event_driven (sibling degrades independently)")
}
}
func TestCompile_PriorModePreservedOverDecideMode(t *testing.T) {
// A durably-persisted SCAN_ONLY (or DISABLED, or any degraded mode)
// must not be re-promoted to EVENT_DRIVEN by decideMode on every
// Compile. Otherwise lag-fallback / operator pause / manual scan-only
// don't survive an engine rebuild.
rule := ruleExpDays("r", "x/", 30)
rh := s3lifecycle.RuleHash(rule)
key := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{
PriorStates: map[s3lifecycle.ActionKey]PriorState{
key: {BootstrapComplete: true, Mode: ModeScanOnly},
},
})
if snap.actions[key].Mode != ModeScanOnly {
t.Fatalf("durable Mode=ScanOnly should win over decideMode, got %v", snap.actions[key].Mode)
}
if snap.actions[key].IsActive() {
t.Fatalf("ScanOnly action must not be active")
}
// And: a missing PriorState falls through to decideMode as before.
rule2 := ruleExpDays("fresh", "x/", 30)
key2 := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: s3lifecycle.RuleHash(rule2), ActionKind: s3lifecycle.ActionKindExpirationDays}
snap2 := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule2}}}, CompileOptions{})
if snap2.actions[key2].Mode != ModeEventDriven {
t.Fatalf("missing prior should fall through to decideMode (EventDriven), got %v", snap2.actions[key2].Mode)
}
}
func TestCompile_ExpirationDateScansAtDate(t *testing.T) {
date := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
rule := &s3lifecycle.Rule{
ID: "d",
Status: s3lifecycle.StatusEnabled,
Prefix: "x/",
ExpirationDate: date,
}
prior := map[s3lifecycle.ActionKey]PriorState{
{RuleHash: s3lifecycle.RuleHash(rule), ActionKind: s3lifecycle.ActionKindExpirationDate}: {BootstrapComplete: true},
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: prior})
if len(snap.dateActions) != 1 {
t.Fatalf("want 1 date action, got %d", len(snap.dateActions))
}
for _, d := range snap.dateActions {
if !d.Equal(date) {
t.Fatalf("date want %v, got %v", date, d)
}
}
}
func TestCompile_DisabledRuleNeverActivates(t *testing.T) {
rule := ruleExpDays("d", "x/", 30)
rule.Status = s3lifecycle.StatusDisabled
prior := map[s3lifecycle.ActionKey]PriorState{
{RuleHash: s3lifecycle.RuleHash(rule), ActionKind: s3lifecycle.ActionKindExpirationDays}: {BootstrapComplete: true},
}
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{PriorStates: prior})
for _, a := range snap.actions {
if a.Mode != ModeDisabled || a.IsActive() {
t.Fatalf("disabled rule must be ModeDisabled and inactive")
}
}
}
func TestSnapshot_MarkActiveFlipsRoutingFilter(t *testing.T) {
rule := ruleExpDays("r", "x/", 30)
e := New()
snap := e.Compile([]CompileInput{{Bucket: "b1", Rules: []*s3lifecycle.Rule{rule}}}, CompileOptions{})
key := s3lifecycle.ActionKey{Bucket: "b1", RuleHash: s3lifecycle.RuleHash(rule), ActionKind: s3lifecycle.ActionKindExpirationDays}
if snap.actions[key].IsActive() {
t.Fatalf("should start inactive")
}
snap.MarkActive(key)
if !snap.actions[key].IsActive() {
t.Fatalf("MarkActive should flip the bit")
}
// MarkActive on a missing key is a no-op (stale callback after rebuild).
snap.MarkActive(s3lifecycle.ActionKey{})
}
func TestCompile_CrossBucketIdenticalRulesDoNotCollide(t *testing.T) {
// Two buckets carry rules whose XML — and therefore RuleHash — is
// identical. ActionKey must be bucket-scoped so the second bucket's
// CompiledAction does not overwrite the first's in snap.actions.
rule := ruleExpDays("shared", "x/", 30)
rh := s3lifecycle.RuleHash(rule)
prior := map[s3lifecycle.ActionKey]PriorState{
{Bucket: "alpha", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}: {BootstrapComplete: true},
{Bucket: "beta", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}: {BootstrapComplete: true},
}
e := New()
snap := e.Compile([]CompileInput{
{Bucket: "alpha", Rules: []*s3lifecycle.Rule{rule}},
{Bucket: "beta", Rules: []*s3lifecycle.Rule{rule}},
}, CompileOptions{PriorStates: prior})
if got := len(snap.actions); got != 2 {
t.Fatalf("want 2 actions (one per bucket), got %d", got)
}
alphaKey := s3lifecycle.ActionKey{Bucket: "alpha", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}
betaKey := s3lifecycle.ActionKey{Bucket: "beta", RuleHash: rh, ActionKind: s3lifecycle.ActionKindExpirationDays}
if snap.actions[alphaKey] == nil || snap.actions[alphaKey].Bucket != "alpha" {
t.Fatalf("alpha bucket action missing or wrong bucket")
}
if snap.actions[betaKey] == nil || snap.actions[betaKey].Bucket != "beta" {
t.Fatalf("beta bucket action missing or wrong bucket")
}
}
func TestEngine_SnapshotAtomicSwap(t *testing.T) {
e := New()
r1 := ruleExpDays("r1", "a/", 1)
snap1 := e.Compile([]CompileInput{{Bucket: "b", Rules: []*s3lifecycle.Rule{r1}}}, CompileOptions{})
if snap1.SnapshotID() == 0 {
t.Fatalf("snapshot id should be > 0")
}
r2 := ruleExpDays("r2", "b/", 2)
snap2 := e.Compile([]CompileInput{{Bucket: "b", Rules: []*s3lifecycle.Rule{r2}}}, CompileOptions{})
if snap2.SnapshotID() <= snap1.SnapshotID() {
t.Fatalf("snapshot id should be monotonic")
}
if e.Snapshot() != snap2 {
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)
}
}
}