Files
s3/weed/pb/s3_lifecycle.proto
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

341 lines
14 KiB
Protocol Buffer

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/<bucket>/<rule_hash_hex>/<action_kind>/state -> LifecycleState
// /etc/s3/lifecycle/<bucket>/<rule_hash_hex>/<action_kind>/pending -> repeated PendingItem
// /etc/s3/lifecycle/<bucket>/_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 <Rule>
// 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/<bucket>/<rule_hash_hex>/<action_kind>/state
//
// A single XML <Rule> 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<int64, FilerShardCursorList> 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;
}