1use bytes::{BufMut, Bytes, BytesMut};
5use spate_core::deser::Owned;
6use spate_core::error::{ErrorClass, SinkError};
7use spate_core::metrics::Meter;
8use spate_core::record::Record;
9use spate_core::sink::{
10 RowEncoder, SealedBatch, ShardWriter, SinkBundle, SinkParts, SinkPoolConfig, endpoint_probe,
11};
12use std::collections::{HashMap, VecDeque};
13use std::sync::{Arc, Mutex, MutexGuard};
14use std::time::Duration;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct ReplicaTag {
20 pub shard: usize,
22 pub replica: usize,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28#[non_exhaustive]
29pub enum ScriptedResult {
30 Ok,
32 Retryable(String),
34 Fatal(String),
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40#[non_exhaustive]
41pub struct WriteOutcome {
42 pub delay: Option<Duration>,
45 pub result: ScriptedResult,
47}
48
49impl WriteOutcome {
50 #[must_use]
52 pub fn ok() -> Self {
53 WriteOutcome {
54 delay: None,
55 result: ScriptedResult::Ok,
56 }
57 }
58
59 #[must_use]
61 pub fn retryable(reason: impl Into<String>) -> Self {
62 WriteOutcome {
63 delay: None,
64 result: ScriptedResult::Retryable(reason.into()),
65 }
66 }
67
68 #[must_use]
70 pub fn fatal(reason: impl Into<String>) -> Self {
71 WriteOutcome {
72 delay: None,
73 result: ScriptedResult::Fatal(reason.into()),
74 }
75 }
76
77 #[must_use]
79 pub fn after(mut self, delay: Duration) -> Self {
80 self.delay = Some(delay);
81 self
82 }
83}
84
85#[derive(Clone, Debug)]
87pub struct CapturedWrite {
88 pub shard: usize,
90 pub replica: usize,
92 pub rows: u64,
94 pub bytes: u64,
96 pub dedup_token: String,
98 pub payload: Vec<u8>,
101 pub result: ScriptedResult,
103}
104
105#[derive(Debug, Default)]
106struct SinkState {
107 global: VecDeque<WriteOutcome>,
108 per_endpoint: HashMap<(usize, usize), VecDeque<WriteOutcome>>,
109 probe_failures: HashMap<(usize, usize), String>,
110 writes: Vec<CapturedWrite>,
111}
112
113#[must_use]
115pub fn capture_writer() -> (CaptureWriter, SinkScript) {
116 let state = Arc::new(Mutex::new(SinkState::default()));
117 (
118 CaptureWriter {
119 state: Arc::clone(&state),
120 },
121 SinkScript { state },
122 )
123}
124
125#[derive(Clone, Debug)]
128pub struct SinkScript {
129 state: Arc<Mutex<SinkState>>,
130}
131
132impl SinkScript {
133 fn lock(&self) -> MutexGuard<'_, SinkState> {
134 self.state.lock().expect("spate-test sink state poisoned")
135 }
136
137 pub fn enqueue_global(&self, outcome: WriteOutcome) {
140 self.lock().global.push_back(outcome);
141 }
142
143 pub fn enqueue_for(&self, shard: usize, replica: usize, outcome: WriteOutcome) {
145 self.lock()
146 .per_endpoint
147 .entry((shard, replica))
148 .or_default()
149 .push_back(outcome);
150 }
151
152 pub fn fail_probe(&self, shard: usize, replica: usize, reason: impl Into<String>) {
155 self.lock()
156 .probe_failures
157 .insert((shard, replica), reason.into());
158 }
159
160 pub fn heal_probe(&self, shard: usize, replica: usize) {
162 self.lock().probe_failures.remove(&(shard, replica));
163 }
164
165 #[must_use]
167 pub fn writes(&self) -> Vec<CapturedWrite> {
168 self.lock().writes.clone()
169 }
170}
171
172#[derive(Clone, Debug)]
176pub struct CaptureWriter {
177 state: Arc<Mutex<SinkState>>,
178}
179
180impl ShardWriter for CaptureWriter {
181 type Endpoint = ReplicaTag;
182
183 fn attach_metrics(&mut self, meter: Option<Meter>) {
184 if let Some(meter) = meter {
187 meter.counter("attaches_total", &[]).increment(1);
188 }
189 }
190
191 fn write_batch(
192 &self,
193 endpoint: &ReplicaTag,
194 batch: &SealedBatch,
195 ) -> impl Future<Output = Result<(), SinkError>> + Send {
196 let state = Arc::clone(&self.state);
197 let endpoint = *endpoint;
198 let rows = batch.rows;
199 let bytes = batch.bytes;
200 let dedup_token = batch.dedup_token.clone();
201 let payload: Vec<u8> = batch
202 .frames
203 .iter()
204 .flat_map(|f: &Bytes| f.iter().copied())
205 .collect();
206 async move {
207 let outcome = {
208 let mut st = state.lock().expect("spate-test sink state poisoned");
209 let outcome = st
210 .per_endpoint
211 .get_mut(&(endpoint.shard, endpoint.replica))
212 .and_then(VecDeque::pop_front)
213 .or_else(|| st.global.pop_front())
214 .unwrap_or_else(WriteOutcome::ok);
215 st.writes.push(CapturedWrite {
216 shard: endpoint.shard,
217 replica: endpoint.replica,
218 rows,
219 bytes,
220 dedup_token,
221 payload,
222 result: outcome.result.clone(),
223 });
224 outcome
225 };
226 if let Some(delay) = outcome.delay {
227 tokio::time::sleep(delay).await;
228 }
229 match outcome.result {
230 ScriptedResult::Ok => Ok(()),
231 ScriptedResult::Retryable(reason) => Err(SinkError::Client {
232 class: ErrorClass::Retryable,
233 reason,
234 }),
235 ScriptedResult::Fatal(reason) => Err(SinkError::Client {
236 class: ErrorClass::Fatal,
237 reason,
238 }),
239 }
240 }
241 }
242
243 fn probe(&self, endpoint: &ReplicaTag) -> impl Future<Output = Result<(), SinkError>> + Send {
244 let state = Arc::clone(&self.state);
245 let endpoint = *endpoint;
246 async move {
247 let failure = state
248 .lock()
249 .expect("spate-test sink state poisoned")
250 .probe_failures
251 .get(&(endpoint.shard, endpoint.replica))
252 .cloned();
253 match failure {
254 None => Ok(()),
255 Some(reason) => Err(SinkError::Client {
256 class: ErrorClass::Retryable,
257 reason,
258 }),
259 }
260 }
261 }
262}
263
264#[must_use]
273pub fn capture_sink(shards: usize, replicas: usize) -> (CaptureSink, SinkScript) {
274 assert!(shards > 0, "capture_sink needs at least one shard");
275 assert!(replicas > 0, "capture_sink needs at least one replica");
276 let (writer, script) = capture_writer();
277 (
278 CaptureSink {
279 writer,
280 shards,
281 replicas,
282 pool: SinkPoolConfig::default(),
283 },
284 script,
285 )
286}
287
288#[derive(Clone, Debug)]
290pub struct CaptureSink {
291 writer: CaptureWriter,
292 shards: usize,
293 replicas: usize,
294 pool: SinkPoolConfig,
295}
296
297impl CaptureSink {
298 #[must_use]
300 pub fn with_pool_config(mut self, pool: SinkPoolConfig) -> Self {
301 self.pool = pool;
302 self
303 }
304}
305
306impl SinkBundle for CaptureSink {
307 type Writer = CaptureWriter;
308
309 fn into_parts(self) -> SinkParts<CaptureWriter> {
310 let endpoints: Vec<Vec<ReplicaTag>> = (0..self.shards)
311 .map(|shard| {
312 (0..self.replicas)
313 .map(|replica| ReplicaTag { shard, replica })
314 .collect()
315 })
316 .collect();
317 let probe = endpoint_probe(self.writer.clone(), Arc::new(endpoints.clone()));
322 SinkParts::new(self.writer, endpoints, self.pool)
323 .with_component_type("capture")
324 .with_probe(probe)
325 }
326}
327
328#[derive(Clone, Copy, Debug, Default)]
331pub struct TestEncoder;
332
333impl RowEncoder<Owned<Vec<u8>>> for TestEncoder {
334 fn encode<'buf>(&mut self, rec: &Record<Vec<u8>>, buf: &mut BytesMut) -> Result<(), SinkError> {
335 let len = u32::try_from(rec.payload.len()).map_err(|_| SinkError::Client {
336 class: ErrorClass::RecordLevel,
337 reason: "row longer than u32::MAX".to_owned(),
338 })?;
339 buf.put_u32_le(len);
340 buf.extend_from_slice(&rec.payload);
341 Ok(())
342 }
343}
344
345#[must_use]
351pub fn decode_rows(mut bytes: &[u8]) -> Vec<Vec<u8>> {
352 let mut rows = Vec::new();
353 while !bytes.is_empty() {
354 assert!(bytes.len() >= 4, "decode_rows: truncated length prefix");
355 let len = u32::from_le_bytes(bytes[..4].try_into().expect("4 bytes")) as usize;
356 bytes = &bytes[4..];
357 assert!(bytes.len() >= len, "decode_rows: truncated row body");
358 rows.push(bytes[..len].to_vec());
359 bytes = &bytes[len..];
360 }
361 rows
362}