package engine import ( "bytes" "sort" "sync/atomic" "time" "github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle" ) var snapshotIDSeq atomic.Uint64 // CompileInput: at most one entry per Bucket. Duplicates would overwrite // earlier entries' BucketIndex. type CompileInput struct { Bucket string Rules []*s3lifecycle.Rule Versioned bool } // PriorState carries durable per-action state into Compile. Missing keys // are treated as bootstrap_complete=false. type PriorState struct { BootstrapComplete bool Mode RuleMode } // MetaLogRetention=0 means unbounded; the retention gate doesn't trip. type CompileOptions struct { MetaLogRetention time.Duration BootstrapLookbackMin time.Duration PriorStates map[s3lifecycle.ActionKey]PriorState } const defaultBootstrapLookbackMin = 5 * s3lifecycle.SmallDelay func (e *Engine) Compile(inputs []CompileInput, opts CompileOptions) *Snapshot { if opts.BootstrapLookbackMin == 0 { opts.BootstrapLookbackMin = defaultBootstrapLookbackMin } snap := &Snapshot{ id: snapshotIDSeq.Add(1), buckets: make(map[string]*BucketIndex), actions: make(map[s3lifecycle.ActionKey]*CompiledAction), originalDelayGroups: make(map[time.Duration][]s3lifecycle.ActionKey), dateActions: make(map[s3lifecycle.ActionKey]time.Time), } for _, in := range inputs { bi := &BucketIndex{bucket: in.Bucket, versioned: in.Versioned} snap.buckets[in.Bucket] = bi for _, rule := range in.Rules { ruleHash := s3lifecycle.RuleHash(rule) for _, kind := range s3lifecycle.RuleActionKinds(rule) { key := s3lifecycle.ActionKey{Bucket: in.Bucket, RuleHash: ruleHash, ActionKind: kind} // Durable Mode wins; decideMode is only the seed for new // or legacy-zero actions. Otherwise lag-fallback / operator // SCAN_ONLY would re-promote on every rebuild. prior, hasPrior := opts.PriorStates[key] mode := prior.Mode if !hasPrior || mode == ModeUnspecified { mode = decideMode(rule, kind, opts.MetaLogRetention, opts.BootstrapLookbackMin) } // Active gates routing: MatchPath / MatchOriginalWrite skip // !IsActive actions. ScanAtDate's only dispatch path is the // bootstrap walk's MatchPath call, so the action must be // considered active there or its rule is silently a no-op. // Bootstrap-completion state is per-action and event-driven- // shaped; date-based actions don't need a bootstrap rendezvous. active := mode == ModeScanAtDate || (prior.BootstrapComplete && mode == ModeEventDriven) ca := &CompiledAction{ Rule: rule, Bucket: in.Bucket, Key: key, Delay: s3lifecycle.MinTriggerAge(rule, kind), PredicateSensitive: rulePredicateSensitive(rule), Mode: mode, } if active { ca.markActive() } snap.actions[key] = ca bi.actionKeys = append(bi.actionKeys, key) // Index every action regardless of `active`; routing // re-filters on IsActive() so MarkActive flips are visible // without a recompile. if mode == ModeScanAtDate { snap.dateActions[key] = rule.ExpirationDate } if mode == ModeEventDriven { snap.originalDelayGroups[ca.Delay] = append(snap.originalDelayGroups[ca.Delay], key) if ca.PredicateSensitive { snap.predicateActions = append(snap.predicateActions, key) } } } } } snap.allActionsSorted = make([]*CompiledAction, 0, len(snap.actions)) for _, a := range snap.actions { snap.allActionsSorted = append(snap.allActionsSorted, a) } sort.Slice(snap.allActionsSorted, func(i, j int) bool { a, b := snap.allActionsSorted[i], snap.allActionsSorted[j] if a.Bucket != b.Bucket { return a.Bucket < b.Bucket } if c := bytes.Compare(a.Key.RuleHash[:], b.Key.RuleHash[:]); c != 0 { return c < 0 } return a.Key.ActionKind < b.Key.ActionKind }) e.current.Store(snap) return snap } // rulePredicateSensitive: only tag filters can flip post-PUT. Size is // immutable once written; a size change is a fresh write through the // original-write stream. func rulePredicateSensitive(rule *s3lifecycle.Rule) bool { if rule == nil { return false } return len(rule.FilterTags) > 0 }