syntax = "proto3"; package s3_lifecycle_pb; option go_package = "github.com/seaweedfs/seaweedfs/weed/pb/s3_lifecycle_pb"; option java_package = "seaweedfs.client"; option java_outer_classname = "S3LifecycleProto"; // Storage layout reference (see S3_LIFECYCLE_REDESIGN.md): // // /etc/s3/lifecycle////state -> LifecycleState // /etc/s3/lifecycle////pending -> repeated PendingItem // /etc/s3/lifecycle//_bootstrap -> BootstrapState // /etc/s3/lifecycle/_reader/reader_state -> ReaderState // /etc/s3/lifecycle/_reader/blockers -> repeated BlockerRecord // /etc/s3/lifecycle/_reader/retry_budget -> repeated RetryBudgetEntry // // State is per-(rule_hash, action_kind), not per-rule: a single XML // with N action sub-elements expands into N compiled actions, each with // its own state file and its own pending file. Sibling actions of the // same rule can degrade or activate independently. // ActionKind classifies the lifecycle action a single compiled entry // represents. A single XML rule may produce multiple compiled entries — // one per populated action. enum ActionKind { ACTION_KIND_UNSPECIFIED = 0; EXPIRATION_DAYS = 1; // Expiration.Days EXPIRATION_DATE = 2; // Expiration.Date NONCURRENT_DAYS = 3; // NoncurrentVersionExpiration.NoncurrentDays (with optional NewerNoncurrent retention) NEWER_NONCURRENT = 4; // NoncurrentVersionExpiration.NewerNoncurrentVersions, count-only (no NoncurrentDays) ABORT_MPU = 5; // AbortIncompleteMultipartUpload.DaysAfterInitiation EXPIRED_DELETE_MARKER = 6; // Expiration.ExpiredObjectDeleteMarker } // SeaweedS3LifecycleInternal is the worker-to-S3 service that performs the // actual lifecycle deletion. The lifecycle worker computes the (rule, action) // verdict locally; the S3 server is the only component allowed to mutate the // filer state, so it gets the final word: it re-fetches the live entry, // verifies the EntryIdentity CAS, runs object-lock protections, and dispatches // to the appropriate internal helper. service SeaweedS3LifecycleInternal { rpc LifecycleDelete(LifecycleDeleteRequest) returns (LifecycleDeleteResponse); } // LifecycleDeleteOutcome captures every per-event verdict the worker needs. // Cursor advance / pending mutation rules: // // DONE -> advance cursor / drop pending // NOOP_RESOLVED -> advance cursor / drop pending (object already gone or stale identity) // SKIPPED_OBJECT_LOCK -> log + counter; advance cursor (per design: object lock is operator concern) // RETRY_LATER -> hold cursor; feed retry-budget; next batch retries from same position // BLOCKED -> hold cursor; durable BlockerRecord written; operator must intervene enum LifecycleDeleteOutcome { LIFECYCLE_DELETE_OUTCOME_UNSPECIFIED = 0; DONE = 1; NOOP_RESOLVED = 2; SKIPPED_OBJECT_LOCK = 3; RETRY_LATER = 4; BLOCKED = 5; } message LifecycleDeleteRequest { // Routing. string bucket = 1; string object_path = 2; // bucket-relative; no leading slash string version_id = 3; // empty for non-versioned bytes rule_hash = 4; // 8 bytes; matches the rule's per-rule dir ActionKind action_kind = 5; // chooses dispatch within the rule // Stream context (echoed in BlockerRecord on FATAL outcomes so operators // can resolve the right (shard, delay) tuple). string stream_shard = 10; int64 stream_delay_seconds = 11; int64 stream_position_ts_ns = 12; int64 stream_position_offset = 13; // CAS witness. The server re-fetches the live entry and aborts as // NOOP_RESOLVED (STALE_IDENTITY) if any field doesn't match. EntryIdentity expected_identity = 20; // Snapshot id at the time the worker computed this verdict; the server // verifies the (rule_hash, action_kind) is still in the current policy // snapshot and aborts as NOOP_RESOLVED (STALE_POLICY) if not. uint64 engine_snapshot_id = 21; } message LifecycleDeleteResponse { LifecycleDeleteOutcome outcome = 1; // Human-readable cause, e.g. "STALE_IDENTITY: mtime drift", // "FATAL_EVENT_ERROR: malformed entry", or "TRANSPORT_ERROR: filer // unavailable". Echoed into BlockerRecord.last_error on FATAL. string reason = 2; } // MessagePosition mirrors weed/util/log_buffer.MessagePosition for durable // storage. ts_ns + offset uniquely identifies a position within a per-filer // log buffer; per-shard cursors (see ReaderState) use this shape to skip past // already-processed events when replaying. message MessagePosition { int64 ts_ns = 1; int64 offset = 2; } // Cursor map keyed by filer_id. Each delay group's cursor (ReaderState) and // the predicate cursor (ReaderState) are per-filer-shard maps because // LogEntry.Offset is per-filer and not globally unique. message FilerShardCursor { string filer_id = 1; MessagePosition position = 2; } // EntryIdentity is the CAS witness used by LifecycleDelete to detect that // the live entry hasn't changed between worker decision and server execution. // Mismatch -> STALE_IDENTITY -> NOOP_RESOLVED. message EntryIdentity { int64 mtime_ns = 1; int64 size = 2; string head_fid = 3; bytes extended_hash = 4; // sha256 over sorted Extended map; cheap and stable } // LifecycleState captures the durable per-action scheduling state. Keyed by // (rule_hash, action_kind); persisted at // /etc/s3/lifecycle////state // // A single XML with N populated action sub-elements expands into N // LifecycleState records, one per sibling action. message LifecycleState { // Proto3 best practice: zero value is an UNSPECIFIED sentinel so that // legacy / partially-populated payloads don't silently default to a // semantically active value. Persisted state files always set explicit // non-zero values; reading UNSPECIFIED indicates corruption or a missing // field. enum RuleMode { RULE_MODE_UNSPECIFIED = 0; EVENT_DRIVEN = 1; SCAN_AT_DATE = 2; SCAN_ONLY = 3; DISABLED = 4; PENDING_BOOTSTRAP = 5; } // DEGRADED_REASON_UNSPECIFIED replaces the old NONE sentinel: an unset // / zero degraded_reason means "no active degradation" — operators // treat UNSPECIFIED as healthy. enum DegradedReason { DEGRADED_REASON_UNSPECIFIED = 0; LAG_HIGH = 1; PENDING_FULL = 2; DELETE_FAILURES = 3; OPERATOR_PAUSED = 4; RETENTION_BELOW_HORIZON = 5; LOST_LOG = 6; } bytes rule_hash = 1; // 8 bytes; matches the parent rule dir ActionKind action_kind = 2; // matches the leaf dir name string rule_id = 3; // display only; identical across sibling actions RuleMode mode = 4; DegradedReason degraded_reason = 5; int64 degraded_since_ns = 6; bool bootstrap_complete = 7; int64 bootstrap_started_at_ns = 8; int64 bootstrap_completed_at_ns = 9; // Safety-scan scheduling. Set by the safety-scan job when a SCAN_ONLY // (or periodic safety-scan) bootstrap pass completes; consulted by the // detector to decide whether to emit the next bootstrap task. int64 last_safety_scan_ts_ns = 10; int64 next_safety_scan_ts_ns = 11; // Counters for observability; no behavior depends on these. // - evaluated_total: live entries the worker considered for this action // (filter-matched + due-checked); includes objects that ended up not // yet eligible. // - expired_total: successful DONE outcomes (object/version/marker/MPU // removed) under this action. // - metadata_only_total: subset of expired_total where the worker // short-circuited via volume-TTL routing (chunks left to volume GC). // - error_total: outcomes that did NOT advance the cursor under this // action (RETRY_LATER, BLOCKED, FATAL_EVENT_ERROR). int64 evaluated_total = 12; int64 expired_total = 13; int64 metadata_only_total = 14; int64 error_total = 15; } // PendingItem records a not-yet-due eligibility for late-predicate exceptions // (e.g. tag added at age 30d on a 60d rule). Drained by s3.lifecycle.drain. // One use only — not the steady-state path. message PendingItem { string object_path = 1; string version_id = 2; int64 due_at_ns = 3; EntryIdentity expected_identity = 4; } // BootstrapState tracks the bucket walker's resume point. Operator-resolution // of a BOOTSTRAP-stream blocker does NOT mutate this field — the next // scheduled bootstrap task picks up from last_scanned_path and re-walks. message BootstrapState { string last_scanned_path = 1; int64 started_at_ns = 2; int64 completed_at_ns = 3; // zero while in progress } // ReaderState is the cluster-singleton reader's durable cursors. One file at // /etc/s3/lifecycle/_reader/reader_state, written by the singleton task. message ReaderState { string primary_filer_endpoint = 1; // Per-delay-group, per-filer-shard cursors. Outer key is delay_seconds. // Inner FilerShardCursor list maps filer_id -> MessagePosition. map last_processed_original = 2; // Predicate-change pass cursor, keyed by filer_id. repeated FilerShardCursor last_processed_predicate = 3; // Filer-shard keys whose cursor reached range.latest and were safely GC'd. // Used by the lost-log gate at task entry to distinguish "F was tail-drained // safely then pruned" from "F's logs vanished before catch-up". Encoded as // stream-specific keys (same shape as RetryBudgetEntry.StreamKey). repeated TailDrainedKey tail_drained_streams = 4; int64 last_checkpoint_ns = 5; } message FilerShardCursorList { repeated FilerShardCursor cursors = 1; } // TailDrainedKey identifies a (stream_kind, shard, [delay]) tuple whose // cursor previously caught up to range.latest. Persisted in ReaderState so // the lost-log gate can distinguish safe drain from data loss. message TailDrainedKey { StreamKind stream_kind = 1; string filer_id = 2; int64 delay_seconds = 3; // populated when stream_kind == ORIGINAL } // StreamKind classifies the four blockable streams. ORIGINAL/PREDICATE pause // reader cursors; BOOTSTRAP pauses a bucket walker; PENDING pauses a rule's // drain task. Zero is an UNSPECIFIED sentinel — a BlockerRecord whose // stream_kind reads as UNSPECIFIED is a corruption signal, not a default. enum StreamKind { STREAM_KIND_UNSPECIFIED = 0; ORIGINAL = 1; PREDICATE = 2; BOOTSTRAP = 3; PENDING = 4; } // BlockerRecord is the durable record of a paused stream. The cursor stays // at the failing position until the operator clears it via // `s3.lifecycle.blockers retry|resume|quarantine`. There is no automatic // dead-letter — silently routing a failed delete decision aside is unsafe // for lifecycle. message BlockerRecord { StreamKind stream_kind = 1; // Stream-specific scoping; populated per stream_kind. string shard = 2; // filer_id; ORIGINAL/PREDICATE only int64 delay_seconds = 3; // ORIGINAL only MessagePosition position = 4; // ORIGINAL/PREDICATE only // Common context. bucket/object_path/version_id always populated. // rule_hash and action_kind are OPTIONAL: empty / UNSPECIFIED for // pre-evaluation failures (e.g. handleEvent fetchLive FATAL where no // action has been evaluated). When populated, both are set together — // the failure is bound to a specific (rule, action) pair. bytes rule_hash = 5; string bucket = 6; string object_path = 7; string version_id = 8; ActionKind action_kind = 14; // optional; UNSPECIFIED for pre-evaluation failures string reason = 9; // "FATAL_EVENT_ERROR: malformed entry" string last_error = 10; // raw error string from the last attempt int64 first_seen_at_ns = 11; int64 last_retry_at_ns = 12; int32 retry_count = 13; } // StreamKey is the shape used by retry-budget tracking. One of four shapes // per stream_kind, since the identifying tuple differs. message StreamKey { oneof key { OriginalKey original = 1; PredicateKey predicate = 2; BootstrapKey bootstrap = 3; PendingKey pending = 4; } } message OriginalKey { string shard = 1; int64 delay_seconds = 2; MessagePosition position = 3; } message PredicateKey { string shard = 1; MessagePosition position = 2; } message BootstrapKey { string bucket = 1; string object_path = 2; string version_id = 3; bytes rule_hash = 4; ActionKind action_kind = 5; // distinguishes per-action streams under one rule } message PendingKey { string bucket = 1; bytes rule_hash = 2; string object_path = 3; string version_id = 4; ActionKind action_kind = 5; } // RetryBudgetEntry tracks consecutive RETRY_LATER outcomes for a stream key. // Promotes to BLOCKED when consecutive_retries >= retryBudgetMax (30) or age // >= retryBudgetMaxAge (4h). Compacts on success (clearRetryBudget). message RetryBudgetEntry { StreamKey key = 1; int32 consecutive_retries = 2; int64 first_seen_at_ns = 3; int64 last_retry_at_ns = 4; } // RetryTarget carries routing info needed to (a) write a BlockerRecord on // promotion and (b) re-invoke the right primitive on operator retry. We // deliberately do NOT carry expected_identity or action: the handler // re-fetches live state and re-evaluates from scratch. message RetryTarget { StreamKey key = 1; string bucket = 2; string object_path = 3; string version_id = 4; bytes rule_hash = 5; ActionKind action_kind = 6; }