mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-20 04:54:24 +02:00
* s3 lifecycle: replay only the rules that fit the retention window runShard handed drainShardEvents the full snapshot, so rules the retention window assigns to the walk were also matched during meta-log replay. Their matches are not due yet, and drainShardEvents freezes the cursor at the first event with a not-yet-due match. With a 3-day and a 30-day expiration rule on one bucket and RetentionWindow of 8 days, every shard's cursor stopped at the first 30-day object's PUT and stayed there until that object came due. Each pass then re-read the log from that point and re-dispatched every 3-day expiration already handled, which surfaces as NOOP_RESOLVED dispatches, a growing cursor_lag_max, and passes that still report status=ok. Snapshot.ReplayView drops the replay-kind actions promoted to the walk and keeps every other action as compiled, and the drain uses it. Promoted rules are served by the walk as before. RulesForShard, PromotedHash and ReplayView share one isPromoted predicate. With RetentionWindow unset no active rule is promoted, so behaviour without a configured window is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3 lifecycle: scale the replay-view test with DaysToDuration The test took its retention window from DaysToDuration but dated its PUTs in hours, so under the s3tests build tag (one lifecycle day = 10s) every event fell before the seeded cursor and replay never ran. Express the event ages in DaysToDuration too, so the test exercises replay at both scales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3 lifecycle: rehabilitate scan_only mode in the replay drain view ReplayView shared the base CompiledAction pointers, so a replay-kind rule locked into ModeScanOnly by a prior compile stayed invisible to router.Route's event_driven gate even when the runtime retention window kept it in the replay partition - and it is absent from the walk view too, so the rule would be served by neither. Clone non-promoted replay-kind actions the way RulesForShard does (Mode forced to event_driven), reactivating the scan_only case whose bootstrap rendezvous already completed, while preserving the base active bit for genuinely pre-bootstrap actions. Disabled actions are excluded for parity with the other views, and DESIGN.md now documents the view. * s3 lifecycle: keep drain activation observable, require walker with retention ReplayView now shares pointers for event-driven actions so a mid-pass MarkActive flip remains visible to the drain; only a scan_only-locked action is cloned to event_driven, and reactivated only when its bootstrap rendezvous completed (tracked via a new bootstrapComplete field on CompiledAction). validate() rejects RetentionWindow without Walker: promoted rules would lose their only dispatch path while the drain cursor still advanced past their events. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
202 lines
6.4 KiB
Go
202 lines
6.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"hash"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3lifecycle"
|
|
)
|
|
|
|
// hashItem pairs a ruleset member with its parent action so the
|
|
// sort+hash helpers below can be type-agnostic. Internal type — every
|
|
// hash callsite collects items into a slice, then hands it off.
|
|
type hashItem struct {
|
|
key s3lifecycle.ActionKey
|
|
action *CompiledAction
|
|
}
|
|
|
|
// sortHashItems orders items by (RuleHash, ActionKind, Bucket). The
|
|
// composition matches the ActionKey identity model: RuleHash is the
|
|
// primary content-derived identifier, ActionKind disambiguates siblings
|
|
// of one rule, Bucket scopes by bucket so the same XML in two buckets
|
|
// hashes distinctly. Shared between ReplayContentHash and PromotedHash
|
|
// so the two helpers agree on the on-wire ordering — if one drifted,
|
|
// the cursor could see "rule changed" on a no-op snapshot rebuild.
|
|
func sortHashItems(items []hashItem) {
|
|
sort.Slice(items, func(i, j int) bool {
|
|
if c := bytes.Compare(items[i].key.RuleHash[:], items[j].key.RuleHash[:]); c != 0 {
|
|
return c < 0
|
|
}
|
|
if items[i].key.ActionKind != items[j].key.ActionKind {
|
|
return items[i].key.ActionKind < items[j].key.ActionKind
|
|
}
|
|
return items[i].key.Bucket < items[j].key.Bucket
|
|
})
|
|
}
|
|
|
|
// hashWriter is the small varint-tagged writer the hash helpers use.
|
|
// Each field gets a one-byte tag so a future schema change (new field)
|
|
// can extend the on-wire format without colliding with existing values.
|
|
type hashWriter struct {
|
|
h hash.Hash
|
|
lenbuf [binary.MaxVarintLen64]byte
|
|
}
|
|
|
|
func newHashWriter() *hashWriter {
|
|
return &hashWriter{h: sha256.New()}
|
|
}
|
|
|
|
// writeField writes a length-prefixed byte field under tag.
|
|
func (w *hashWriter) writeField(tag byte, b []byte) {
|
|
_, _ = w.h.Write([]byte{tag})
|
|
n := binary.PutUvarint(w.lenbuf[:], uint64(len(b)))
|
|
_, _ = w.h.Write(w.lenbuf[:n])
|
|
_, _ = w.h.Write(b)
|
|
}
|
|
|
|
// writeInt writes a varint-encoded signed integer under tag.
|
|
func (w *hashWriter) writeInt(tag byte, v int64) {
|
|
_, _ = w.h.Write([]byte{tag})
|
|
n := binary.PutVarint(w.lenbuf[:], v)
|
|
_, _ = w.h.Write(w.lenbuf[:n])
|
|
}
|
|
|
|
func (w *hashWriter) sum() [32]byte {
|
|
var out [32]byte
|
|
copy(out[:], w.h.Sum(nil))
|
|
return out
|
|
}
|
|
|
|
// ReplayContentHash hashes the content (action kind, predicate, TTL value)
|
|
// of every replay-eligible compiled action in the base snapshot, returning
|
|
// the empty hash when no replay-eligible action exists. The hash is:
|
|
// - Partition-independent. A retention-driven scan_only promotion does
|
|
// NOT change this hash; only the dispatch path changes, not the rule
|
|
// content. (PromotedHash exists to catch partition flips separately.)
|
|
// - Stable across snapshot reorderings. Actions are pre-sorted by
|
|
// RuleHash + ActionKind + Bucket so two snapshots with the same rules
|
|
// compiled in any order hash identically.
|
|
// - Disabled-rule-aware. ModeDisabled actions are excluded so disabling
|
|
// a rule changes the hash (it changed the rule set the worker is
|
|
// scanning under).
|
|
//
|
|
// Used as cursor.RuleSetHash. A mismatch between persisted and current
|
|
// triggers the recovery branch on next daily_run.
|
|
func ReplayContentHash(s *Snapshot) [32]byte {
|
|
var empty [32]byte
|
|
if s == nil {
|
|
return empty
|
|
}
|
|
var items []hashItem
|
|
for k, a := range s.actions {
|
|
if a == nil || a.Mode == ModeDisabled {
|
|
continue
|
|
}
|
|
if !isReplayKind(k.ActionKind) {
|
|
continue
|
|
}
|
|
items = append(items, hashItem{key: k, action: a})
|
|
}
|
|
if len(items) == 0 {
|
|
return empty
|
|
}
|
|
sortHashItems(items)
|
|
|
|
w := newHashWriter()
|
|
for _, it := range items {
|
|
w.writeField(0x01, []byte(it.key.Bucket))
|
|
w.writeField(0x02, it.key.RuleHash[:])
|
|
w.writeInt(0x03, int64(it.key.ActionKind))
|
|
// RuleHash already covers the predicate (Prefix + FilterTags + size
|
|
// filters) and per-kind TTLs, so we don't need to re-canonicalise
|
|
// the *Rule. But we also include the action's effective TTL
|
|
// directly so that an "effective TTL of 0" (a malformed rule where
|
|
// the kind doesn't match the populated field) is distinguishable
|
|
// from a valid one.
|
|
w.writeInt(0x04, int64(effectiveTTL(it.action)))
|
|
}
|
|
return w.sum()
|
|
}
|
|
|
|
// PromotedHash hashes the set of replay-eligible actions that *would* land
|
|
// in walk (rather than replay) for the given retentionWindow, due to TTL >
|
|
// retentionWindow. Empty hash when no rules are promoted. Takes the SAME
|
|
// retentionWindow value as RulesForShard so the two helpers cannot disagree
|
|
// about partition membership.
|
|
//
|
|
// Detects partition flips in either direction:
|
|
// - replay → walk (retention dropped): rule appears in this hash but
|
|
// didn't before.
|
|
// - walk → replay (retention recovered): rule used to appear here but no
|
|
// longer does.
|
|
//
|
|
// In both cases the persisted hash differs from the freshly computed one,
|
|
// firing the recovery branch.
|
|
//
|
|
// A mismatch with the persisted PromotedHash triggers recovery even when
|
|
// rule content is unchanged.
|
|
func PromotedHash(s *Snapshot, retentionWindow time.Duration) [32]byte {
|
|
var empty [32]byte
|
|
if s == nil {
|
|
return empty
|
|
}
|
|
var items []hashItem
|
|
for k, a := range s.actions {
|
|
if a == nil || a.Mode == ModeDisabled {
|
|
continue
|
|
}
|
|
if !isReplayKind(k.ActionKind) {
|
|
continue
|
|
}
|
|
if !isPromoted(a, retentionWindow) {
|
|
continue
|
|
}
|
|
items = append(items, hashItem{key: k, action: a})
|
|
}
|
|
if len(items) == 0 {
|
|
return empty
|
|
}
|
|
sortHashItems(items)
|
|
|
|
w := newHashWriter()
|
|
for _, it := range items {
|
|
w.writeField(0x01, []byte(it.key.Bucket))
|
|
w.writeField(0x02, it.key.RuleHash[:])
|
|
w.writeInt(0x03, int64(it.key.ActionKind))
|
|
}
|
|
return w.sum()
|
|
}
|
|
|
|
// MaxEffectiveTTL returns the maximum effective TTL across the *active*
|
|
// replay-eligible actions in s. Returns 0 for a nil snapshot or one with no
|
|
// active replay actions; the caller is expected to be in the empty-replay
|
|
// branch already (per the design's sentinel-cursor logic).
|
|
//
|
|
// "Effective TTL" mirrors the partition predicate in views.go: derived from
|
|
// the rule field that matches the action kind. Walker-only action kinds
|
|
// (ExpirationDate / ExpiredDeleteMarker / NewerNoncurrent) contribute
|
|
// nothing — they're either not replay-eligible or, in a walk view, not
|
|
// active in the replay sense.
|
|
func MaxEffectiveTTL(s *Snapshot) time.Duration {
|
|
if s == nil {
|
|
return 0
|
|
}
|
|
var max time.Duration
|
|
for k, a := range s.actions {
|
|
if a == nil || !a.IsActive() {
|
|
continue
|
|
}
|
|
if !isReplayKind(k.ActionKind) {
|
|
continue
|
|
}
|
|
if ttl := effectiveTTL(a); ttl > max {
|
|
max = ttl
|
|
}
|
|
}
|
|
return max
|
|
}
|