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 quota in this group. Monotonic
130    /// aggregate usage is retained for asynchronous accounting. Idempotent —
131    /// purging an absent bucket reports zero removals.
132    PurgeBucket {
133        bucket_id: String,
134    },
135    /// Confirms the leader's background worker has physically reclaimed every
136    /// queued cold-GC entry with `seq <= up_to_seq`; removes them from the
137    /// replicated queue. Idempotent under replay.
138    AckColdGc {
139        up_to_seq: u64,
140    },
141    /// Replaces this group's entire state with a backup snapshot.
142    ///
143    /// Restore-only: the target group must be empty. Travelling as a normal
144    /// replicated command keeps every replica of the restored cluster
145    /// deterministic while the cluster retains its own raft identity and
146    /// membership -- nothing from the backed-up cluster's raft metadata is
147    /// reused.
148    ImportSnapshot {
149        snapshot: Box<StreamSnapshot>,
150    },
151    /// Sets or clears this group's data-plane quota record for a bucket.
152    /// Replicated to every group so each enforces the same local backstop;
153    /// both limits `None` removes the record. Idempotent under replay.
154    SetBucketQuota {
155        bucket_id: String,
156        max_streams: Option<u64>,
157        max_retained_bytes: Option<u64>,
158    },
159}
160
161impl fmt::Display for StreamCommand {
162    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163        match self {
164            Self::CreateBucket { bucket_id } => write!(f, "create_bucket:{bucket_id}"),
165            Self::DeleteBucket { bucket_id } => write!(f, "delete_bucket:{bucket_id}"),
166            Self::CreateStream { stream_id, .. } => write!(f, "create_stream:{stream_id}"),
167            Self::CreateExternal {
168                stream_id,
169                initial_payload,
170                ..
171            } => write!(
172                f,
173                "create_external:{stream_id}:{} bytes",
174                initial_payload.payload_len
175            ),
176            Self::Append {
177                stream_id, payload, ..
178            } => write!(f, "append:{stream_id}:{} bytes", payload.len()),
179            Self::AppendExternal {
180                stream_id, payload, ..
181            } => write!(
182                f,
183                "append_external:{stream_id}:{} bytes",
184                payload.payload_len
185            ),
186            Self::AppendBatch {
187                stream_id,
188                payloads,
189                ..
190            } => write!(f, "append_batch:{stream_id}:{} items", payloads.len()),
191            Self::PublishSnapshot {
192                stream_id,
193                snapshot_offset,
194                payload,
195                ..
196            } => write!(
197                f,
198                "publish_snapshot:{stream_id}:{snapshot_offset}:{} bytes",
199                payload.len()
200            ),
201            Self::AdvanceRetention {
202                stream_id,
203                retained_offset,
204                ..
205            } => write!(f, "advance_retention:{stream_id}:{retained_offset}"),
206            Self::TouchStreamAccess {
207                stream_id,
208                renew_ttl,
209                ..
210            } => write!(f, "touch_stream_access:{stream_id}:renew_ttl={renew_ttl}"),
211            Self::UpdateStreamAttrs { stream_id, .. } => {
212                write!(f, "update_stream_attrs:{stream_id}")
213            }
214            Self::FlushCold { stream_id, chunk } => write!(
215                f,
216                "flush_cold:{stream_id}:{}..{}",
217                chunk.start_offset, chunk.end_offset
218            ),
219            Self::CompactCold {
220                stream_id,
221                old_chunks,
222                replacement,
223                ..
224            } => write!(
225                f,
226                "compact_cold:{stream_id}:{} chunks:{}..{}",
227                old_chunks.len(),
228                replacement.start_offset,
229                replacement.end_offset
230            ),
231            Self::Close { stream_id, .. } => write!(f, "close_stream:{stream_id}"),
232            Self::DeleteStream { stream_id } => write!(f, "delete_stream:{stream_id}"),
233            Self::PurgeBucket { bucket_id } => write!(f, "purge_bucket:{bucket_id}"),
234            Self::AckColdGc { up_to_seq } => write!(f, "ack_cold_gc:up_to_seq={up_to_seq}"),
235            Self::ImportSnapshot { snapshot } => write!(
236                f,
237                "import_snapshot:buckets={}:streams={}",
238                snapshot.buckets.len(),
239                snapshot.streams.len()
240            ),
241            Self::SetBucketQuota { bucket_id, .. } => {
242                write!(f, "set_bucket_quota:{bucket_id}")
243            }
244        }
245    }
246}