1use bytes::{BufMut, Bytes, BytesMut};
5use spate_core::deser::Owned;
6use spate_core::error::{ErrorClass, SinkError};
7use spate_core::hash::FastMap;
8use spate_core::record::Record;
9use spate_core::sink::{
10 FrameCodecFactory, RowEncoder, SealedBatch, ShardWriter, SinkBundle, SinkMetricsCtx, SinkParts,
11 SinkPoolConfig, endpoint_probe,
12};
13use std::collections::VecDeque;
14use std::sync::{Arc, Mutex, MutexGuard};
15use std::task::{Poll, Waker};
16use std::time::Duration;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
21pub struct ReplicaTag {
22 pub shard: usize,
24 pub replica: usize,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum ScriptedResult {
32 Ok,
34 Retryable(String),
36 Fatal(String),
38 Pending,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
46#[non_exhaustive]
47pub struct WriteOutcome {
48 pub hold: bool,
50 pub delay: Option<Duration>,
53 pub result: ScriptedResult,
55}
56
57impl WriteOutcome {
58 #[must_use]
60 pub fn ok() -> Self {
61 WriteOutcome {
62 hold: false,
63 delay: None,
64 result: ScriptedResult::Ok,
65 }
66 }
67
68 #[must_use]
70 pub fn retryable(reason: impl Into<String>) -> Self {
71 WriteOutcome {
72 hold: false,
73 delay: None,
74 result: ScriptedResult::Retryable(reason.into()),
75 }
76 }
77
78 #[must_use]
80 pub fn fatal(reason: impl Into<String>) -> Self {
81 WriteOutcome {
82 hold: false,
83 delay: None,
84 result: ScriptedResult::Fatal(reason.into()),
85 }
86 }
87
88 #[must_use]
90 pub fn after(mut self, delay: Duration) -> Self {
91 self.delay = Some(delay);
92 self
93 }
94
95 #[must_use]
103 pub fn held(mut self) -> Self {
104 self.hold = true;
105 self
106 }
107}
108
109#[derive(Clone, Debug)]
111pub struct CapturedWrite {
112 pub shard: usize,
114 pub replica: usize,
116 pub rows: u64,
118 pub bytes: u64,
120 pub dedup_token: String,
122 pub payload: Vec<u8>,
125 pub result: ScriptedResult,
128}
129
130#[derive(Debug, Default)]
131struct SinkState {
132 global: VecDeque<WriteOutcome>,
133 per_endpoint: FastMap<(usize, usize), VecDeque<WriteOutcome>>,
134 probe_failures: FastMap<(usize, usize), String>,
135 writes: Vec<CapturedWrite>,
136 releases: u64,
139 holding: usize,
141 held_wakers: Vec<Waker>,
143}
144
145struct Holding(Arc<Mutex<SinkState>>);
148
149impl Drop for Holding {
150 fn drop(&mut self) {
151 if let Ok(mut st) = self.0.lock() {
152 st.holding -= 1;
153 }
154 }
155}
156
157fn refuse_pending(outcome: &WriteOutcome) {
160 assert!(
161 outcome.result != ScriptedResult::Pending,
162 "a write cannot be scripted to resolve as Pending; script it with WriteOutcome::held()"
163 );
164}
165
166#[must_use]
168pub fn capture_writer() -> (CaptureWriter, SinkScript) {
169 let state = Arc::new(Mutex::new(SinkState::default()));
170 (
171 CaptureWriter {
172 state: Arc::clone(&state),
173 },
174 SinkScript { state },
175 )
176}
177
178#[derive(Clone, Debug)]
181pub struct SinkScript {
182 state: Arc<Mutex<SinkState>>,
183}
184
185impl SinkScript {
186 fn lock(&self) -> MutexGuard<'_, SinkState> {
187 self.state.lock().expect("spate-test sink state poisoned")
188 }
189
190 pub fn enqueue_global(&self, outcome: WriteOutcome) {
197 refuse_pending(&outcome);
198 self.lock().global.push_back(outcome);
199 }
200
201 pub fn enqueue_for(&self, shard: usize, replica: usize, outcome: WriteOutcome) {
207 refuse_pending(&outcome);
208 self.lock()
209 .per_endpoint
210 .entry((shard, replica))
211 .or_default()
212 .push_back(outcome);
213 }
214
215 pub fn fail_probe(&self, shard: usize, replica: usize, reason: impl Into<String>) {
218 self.lock()
219 .probe_failures
220 .insert((shard, replica), reason.into());
221 }
222
223 pub fn heal_probe(&self, shard: usize, replica: usize) {
225 self.lock().probe_failures.remove(&(shard, replica));
226 }
227
228 #[must_use]
233 pub fn writes(&self) -> Vec<CapturedWrite> {
234 self.lock().writes.clone()
235 }
236
237 pub fn release_held(&self) {
243 let wakers = {
244 let mut st = self.lock();
245 st.releases += 1;
246 std::mem::take(&mut st.held_wakers)
247 };
248 for waker in wakers {
249 waker.wake();
250 }
251 }
252
253 #[must_use]
255 pub fn held_writes(&self) -> usize {
256 self.lock().holding
257 }
258}
259
260#[derive(Clone, Debug)]
264pub struct CaptureWriter {
265 state: Arc<Mutex<SinkState>>,
266}
267
268impl ShardWriter for CaptureWriter {
269 type Endpoint = ReplicaTag;
270
271 fn attach_metrics(&mut self, cx: SinkMetricsCtx) {
272 if let Some(meter) = cx.meter {
275 meter.counter("attaches_total", &[]).increment(1);
276 }
277 }
278
279 fn write_batch(
280 &self,
281 endpoint: &ReplicaTag,
282 batch: &SealedBatch,
283 ) -> impl Future<Output = Result<(), SinkError>> + Send {
284 let state = Arc::clone(&self.state);
285 let endpoint = *endpoint;
286 let rows = batch.rows;
287 let bytes = batch.bytes;
288 let dedup_token = batch.dedup_token.clone();
289 let payload: Vec<u8> = batch
290 .frames
291 .iter()
292 .flat_map(|f: &Bytes| f.iter().copied())
293 .collect();
294 async move {
295 let (outcome, hold, at) = {
296 let mut st = state.lock().expect("spate-test sink state poisoned");
297 let outcome = st
298 .per_endpoint
299 .get_mut(&(endpoint.shard, endpoint.replica))
300 .and_then(VecDeque::pop_front)
301 .or_else(|| st.global.pop_front())
302 .unwrap_or_else(WriteOutcome::ok);
303 let at = st.writes.len();
304 st.writes.push(CapturedWrite {
305 shard: endpoint.shard,
306 replica: endpoint.replica,
307 rows,
308 bytes,
309 dedup_token,
310 payload,
311 result: ScriptedResult::Pending,
312 });
313 let hold = outcome.hold.then(|| {
316 st.holding += 1;
317 (st.releases, Holding(Arc::clone(&state)))
318 });
319 (outcome, hold, at)
320 };
321 if let Some((started_at, _holding)) = hold {
322 std::future::poll_fn(|cx| {
323 let mut st = state.lock().expect("spate-test sink state poisoned");
324 if st.releases > started_at {
325 Poll::Ready(())
326 } else {
327 st.held_wakers.push(cx.waker().clone());
328 Poll::Pending
329 }
330 })
331 .await;
332 }
333 if let Some(delay) = outcome.delay {
334 tokio::time::sleep(delay).await;
335 }
336 let resolved = match &outcome.result {
337 ScriptedResult::Ok => Ok(()),
338 ScriptedResult::Retryable(reason) => Err(SinkError::Client {
339 class: ErrorClass::Retryable,
340 reason: reason.clone(),
341 }),
342 ScriptedResult::Fatal(reason) => Err(SinkError::Client {
343 class: ErrorClass::Fatal,
344 reason: reason.clone(),
345 }),
346 ScriptedResult::Pending => {
347 unreachable!("the enqueue methods refuse a Pending outcome")
348 }
349 };
350 state.lock().expect("spate-test sink state poisoned").writes[at].result =
354 outcome.result;
355 resolved
356 }
357 }
358
359 fn probe(&self, endpoint: &ReplicaTag) -> impl Future<Output = Result<(), SinkError>> + Send {
360 let state = Arc::clone(&self.state);
361 let endpoint = *endpoint;
362 async move {
363 let failure = state
364 .lock()
365 .expect("spate-test sink state poisoned")
366 .probe_failures
367 .get(&(endpoint.shard, endpoint.replica))
368 .cloned();
369 match failure {
370 None => Ok(()),
371 Some(reason) => Err(SinkError::Client {
372 class: ErrorClass::Retryable,
373 reason,
374 }),
375 }
376 }
377 }
378}
379
380#[must_use]
389pub fn capture_sink(shards: usize, replicas: usize) -> (CaptureSink, SinkScript) {
390 assert!(shards > 0, "capture_sink needs at least one shard");
391 assert!(replicas > 0, "capture_sink needs at least one replica");
392 let (writer, script) = capture_writer();
393 (
394 CaptureSink {
395 writer,
396 shards,
397 replicas,
398 pool: SinkPoolConfig::default(),
399 codec: None,
400 },
401 script,
402 )
403}
404
405#[derive(Clone, Debug)]
407pub struct CaptureSink {
408 writer: CaptureWriter,
409 shards: usize,
410 replicas: usize,
411 pool: SinkPoolConfig,
412 codec: Option<FrameCodecFactory>,
413}
414
415impl CaptureSink {
416 #[must_use]
418 pub fn with_pool_config(mut self, pool: SinkPoolConfig) -> Self {
419 self.pool = pool;
420 self
421 }
422
423 #[must_use]
426 pub fn with_frame_codec(mut self, codec: FrameCodecFactory) -> Self {
427 self.codec = Some(codec);
428 self
429 }
430}
431
432impl SinkBundle for CaptureSink {
433 type Writer = CaptureWriter;
434
435 fn into_parts(self) -> SinkParts<CaptureWriter> {
436 let endpoints: Vec<Vec<ReplicaTag>> = (0..self.shards)
437 .map(|shard| {
438 (0..self.replicas)
439 .map(|replica| ReplicaTag { shard, replica })
440 .collect()
441 })
442 .collect();
443 let probe = endpoint_probe(self.writer.clone(), Arc::new(endpoints.clone()));
448 let parts = SinkParts::new(self.writer, endpoints, self.pool)
449 .with_component_type("capture")
450 .with_probe(probe);
451 match self.codec {
452 Some(codec) => parts.with_frame_codec(codec),
453 None => parts,
454 }
455 }
456}
457
458#[derive(Clone, Copy, Debug, Default)]
461pub struct TestEncoder;
462
463impl RowEncoder<Owned<Vec<u8>>> for TestEncoder {
464 fn encode<'buf>(&mut self, rec: &Record<Vec<u8>>, buf: &mut BytesMut) -> Result<(), SinkError> {
465 let len = u32::try_from(rec.payload.len()).map_err(|_| SinkError::Client {
466 class: ErrorClass::RecordLevel,
467 reason: "row longer than u32::MAX".to_owned(),
468 })?;
469 buf.put_u32_le(len);
470 buf.extend_from_slice(&rec.payload);
471 Ok(())
472 }
473}
474
475#[must_use]
482pub fn decode_rows(mut bytes: &[u8]) -> Vec<Vec<u8>> {
483 let mut rows = Vec::new();
484 while !bytes.is_empty() {
485 assert!(bytes.len() >= 4, "decode_rows: truncated length prefix");
486 let len = u32::from_le_bytes(bytes[..4].try_into().expect("4 bytes")) as usize;
487 bytes = &bytes[4..];
488 assert!(bytes.len() >= len, "decode_rows: truncated row body");
489 rows.push(bytes[..len].to_vec());
490 bytes = &bytes[len..];
491 }
492 rows
493}