Skip to main content

ursula_stream/
command.rs

1use std::fmt;
2
3use bytes::Bytes;
4use serde::Deserialize;
5use serde::Serialize;
6use ursula_shard::BucketStreamId;
7
8use crate::model::ColdChunkRef;
9use crate::model::ExternalPayloadRef;
10use crate::model::ProducerRequest;
11use crate::model::StreamAttrs;
12use crate::snapshot::StreamSnapshot;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub enum StreamCommand {
16    CreateBucket {
17        bucket_id: String,
18    },
19    DeleteBucket {
20        bucket_id: String,
21    },
22    CreateStream {
23        stream_id: BucketStreamId,
24        content_type: String,
25        initial_payload: Bytes,
26        close_after: bool,
27        stream_seq: Option<String>,
28        producer: Option<ProducerRequest>,
29        stream_ttl_seconds: Option<u64>,
30        stream_expires_at_ms: Option<u64>,
31        // `default` keeps pre-attrs replicated records decodable.
32        #[serde(default)]
33        attrs: Option<StreamAttrs>,
34        now_ms: u64,
35    },
36    CreateExternal {
37        stream_id: BucketStreamId,
38        content_type: String,
39        initial_payload: ExternalPayloadRef,
40        #[serde(default)]
41        record_ends: Vec<u64>,
42        close_after: bool,
43        stream_seq: Option<String>,
44        producer: Option<ProducerRequest>,
45        stream_ttl_seconds: Option<u64>,
46        stream_expires_at_ms: Option<u64>,
47        // `default` keeps pre-attrs replicated records decodable.
48        #[serde(default)]
49        attrs: Option<StreamAttrs>,
50        now_ms: u64,
51    },
52    Append {
53        stream_id: BucketStreamId,
54        content_type: Option<String>,
55        payload: Bytes,
56        close_after: bool,
57        stream_seq: Option<String>,
58        producer: Option<ProducerRequest>,
59        now_ms: u64,
60        record_match: Option<u64>,
61    },
62    AppendExternal {
63        stream_id: BucketStreamId,
64        content_type: Option<String>,
65        payload: ExternalPayloadRef,
66        #[serde(default)]
67        record_ends: Vec<u64>,
68        close_after: bool,
69        stream_seq: Option<String>,
70        producer: Option<ProducerRequest>,
71        now_ms: u64,
72        record_match: Option<u64>,
73    },
74    AppendBatch {
75        stream_id: BucketStreamId,
76        content_type: Option<String>,
77        payloads: Vec<Bytes>,
78        producer: Option<ProducerRequest>,
79        now_ms: u64,
80    },
81    PublishSnapshot {
82        stream_id: BucketStreamId,
83        snapshot_offset: u64,
84        content_type: String,
85        payload: Bytes,
86        #[serde(default)]
87        expected_digest: Option<String>,
88        now_ms: u64,
89    },
90    AdvanceRetention {
91        stream_id: BucketStreamId,
92        retained_offset: u64,
93        now_ms: u64,
94    },
95    TouchStreamAccess {
96        stream_id: BucketStreamId,
97        now_ms: u64,
98        renew_ttl: bool,
99    },
100    UpdateStreamAttrs {
101        stream_id: BucketStreamId,
102        attrs: Option<StreamAttrs>,
103        now_ms: u64,
104    },
105    FlushCold {
106        stream_id: BucketStreamId,
107        chunk: ColdChunkRef,
108    },
109    /// Replaces a contiguous run of immutable cold chunks with one equivalent
110    /// object. The external cold-index page update is completed before this
111    /// command is replicated; applying it schedules the old paths for delayed
112    /// reclamation.
113    CompactCold {
114        stream_id: BucketStreamId,
115        old_chunks: Vec<ColdChunkRef>,
116        replacement: ColdChunkRef,
117        gc_not_before_ms: u64,
118    },
119    Close {
120        stream_id: BucketStreamId,
121        stream_seq: Option<String>,
122        producer: Option<ProducerRequest>,
123        now_ms: u64,
124    },
125    DeleteStream {
126        stream_id: BucketStreamId,
127    },
128    /// Administrator-triggered tenant offboarding: removes every stream in
129    /// the bucket, the bucket itself, and its usage ledger entry in this
130    /// group. Idempotent — purging an absent bucket reports zero removals.
131    PurgeBucket {
132        bucket_id: String,
133    },
134    /// Confirms the leader's background worker has physically reclaimed every
135    /// queued cold-GC entry with `seq <= up_to_seq`; removes them from the
136    /// replicated queue. Idempotent under replay.
137    AckColdGc {
138        up_to_seq: u64,
139    },
140    /// Replaces this group's entire state with a backup snapshot.
141    ///
142    /// Restore-only: the target group must be empty. Travelling as a normal
143    /// replicated command keeps every replica of the restored cluster
144    /// deterministic while the cluster retains its own raft identity and
145    /// membership -- nothing from the backed-up cluster's raft metadata is
146    /// reused.
147    ImportSnapshot {
148        snapshot: Box<StreamSnapshot>,
149    },
150    /// Sets or clears this group's data-plane quota record for a bucket.
151    /// Replicated to every group so each enforces the same local backstop;
152    /// both limits `None` removes the record. Idempotent under replay.
153    SetBucketQuota {
154        bucket_id: String,
155        max_streams: Option<u64>,
156        max_retained_bytes: Option<u64>,
157    },
158}
159
160impl fmt::Display for StreamCommand {
161    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162        match self {
163            Self::CreateBucket { bucket_id } => write!(f, "create_bucket:{bucket_id}"),
164            Self::DeleteBucket { bucket_id } => write!(f, "delete_bucket:{bucket_id}"),
165            Self::CreateStream { stream_id, .. } => write!(f, "create_stream:{stream_id}"),
166            Self::CreateExternal {
167                stream_id,
168                initial_payload,
169                ..
170            } => write!(
171                f,
172                "create_external:{stream_id}:{} bytes",
173                initial_payload.payload_len
174            ),
175            Self::Append {
176                stream_id, payload, ..
177            } => write!(f, "append:{stream_id}:{} bytes", payload.len()),
178            Self::AppendExternal {
179                stream_id, payload, ..
180            } => write!(
181                f,
182                "append_external:{stream_id}:{} bytes",
183                payload.payload_len
184            ),
185            Self::AppendBatch {
186                stream_id,
187                payloads,
188                ..
189            } => write!(f, "append_batch:{stream_id}:{} items", payloads.len()),
190            Self::PublishSnapshot {
191                stream_id,
192                snapshot_offset,
193                payload,
194                ..
195            } => write!(
196                f,
197                "publish_snapshot:{stream_id}:{snapshot_offset}:{} bytes",
198                payload.len()
199            ),
200            Self::AdvanceRetention {
201                stream_id,
202                retained_offset,
203                ..
204            } => write!(f, "advance_retention:{stream_id}:{retained_offset}"),
205            Self::TouchStreamAccess {
206                stream_id,
207                renew_ttl,
208                ..
209            } => write!(f, "touch_stream_access:{stream_id}:renew_ttl={renew_ttl}"),
210            Self::UpdateStreamAttrs { stream_id, .. } => {
211                write!(f, "update_stream_attrs:{stream_id}")
212            }
213            Self::FlushCold { stream_id, chunk } => write!(
214                f,
215                "flush_cold:{stream_id}:{}..{}",
216                chunk.start_offset, chunk.end_offset
217            ),
218            Self::CompactCold {
219                stream_id,
220                old_chunks,
221                replacement,
222                ..
223            } => write!(
224                f,
225                "compact_cold:{stream_id}:{} chunks:{}..{}",
226                old_chunks.len(),
227                replacement.start_offset,
228                replacement.end_offset
229            ),
230            Self::Close { stream_id, .. } => write!(f, "close_stream:{stream_id}"),
231            Self::DeleteStream { stream_id } => write!(f, "delete_stream:{stream_id}"),
232            Self::PurgeBucket { bucket_id } => write!(f, "purge_bucket:{bucket_id}"),
233            Self::AckColdGc { up_to_seq } => write!(f, "ack_cold_gc:up_to_seq={up_to_seq}"),
234            Self::ImportSnapshot { snapshot } => write!(
235                f,
236                "import_snapshot:buckets={}:streams={}",
237                snapshot.buckets.len(),
238                snapshot.streams.len()
239            ),
240            Self::SetBucketQuota { bucket_id, .. } => {
241                write!(f, "set_bucket_quota:{bucket_id}")
242            }
243        }
244    }
245}