use bytes::{BufMut, Bytes, BytesMut};
use spate_core::deser::Owned;
use spate_core::error::{ErrorClass, SinkError};
use spate_core::metrics::Meter;
use spate_core::record::Record;
use spate_core::sink::{
RowEncoder, SealedBatch, ShardWriter, SinkBundle, SinkParts, SinkPoolConfig, endpoint_probe,
};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ReplicaTag {
pub shard: usize,
pub replica: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScriptedResult {
Ok,
Retryable(String),
Fatal(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct WriteOutcome {
pub delay: Option<Duration>,
pub result: ScriptedResult,
}
impl WriteOutcome {
#[must_use]
pub fn ok() -> Self {
WriteOutcome {
delay: None,
result: ScriptedResult::Ok,
}
}
#[must_use]
pub fn retryable(reason: impl Into<String>) -> Self {
WriteOutcome {
delay: None,
result: ScriptedResult::Retryable(reason.into()),
}
}
#[must_use]
pub fn fatal(reason: impl Into<String>) -> Self {
WriteOutcome {
delay: None,
result: ScriptedResult::Fatal(reason.into()),
}
}
#[must_use]
pub fn after(mut self, delay: Duration) -> Self {
self.delay = Some(delay);
self
}
}
#[derive(Clone, Debug)]
pub struct CapturedWrite {
pub shard: usize,
pub replica: usize,
pub rows: u64,
pub bytes: u64,
pub dedup_token: String,
pub payload: Vec<u8>,
pub result: ScriptedResult,
}
#[derive(Debug, Default)]
struct SinkState {
global: VecDeque<WriteOutcome>,
per_endpoint: HashMap<(usize, usize), VecDeque<WriteOutcome>>,
probe_failures: HashMap<(usize, usize), String>,
writes: Vec<CapturedWrite>,
}
#[must_use]
pub fn capture_writer() -> (CaptureWriter, SinkScript) {
let state = Arc::new(Mutex::new(SinkState::default()));
(
CaptureWriter {
state: Arc::clone(&state),
},
SinkScript { state },
)
}
#[derive(Clone, Debug)]
pub struct SinkScript {
state: Arc<Mutex<SinkState>>,
}
impl SinkScript {
fn lock(&self) -> MutexGuard<'_, SinkState> {
self.state.lock().expect("spate-test sink state poisoned")
}
pub fn enqueue_global(&self, outcome: WriteOutcome) {
self.lock().global.push_back(outcome);
}
pub fn enqueue_for(&self, shard: usize, replica: usize, outcome: WriteOutcome) {
self.lock()
.per_endpoint
.entry((shard, replica))
.or_default()
.push_back(outcome);
}
pub fn fail_probe(&self, shard: usize, replica: usize, reason: impl Into<String>) {
self.lock()
.probe_failures
.insert((shard, replica), reason.into());
}
pub fn heal_probe(&self, shard: usize, replica: usize) {
self.lock().probe_failures.remove(&(shard, replica));
}
#[must_use]
pub fn writes(&self) -> Vec<CapturedWrite> {
self.lock().writes.clone()
}
}
#[derive(Clone, Debug)]
pub struct CaptureWriter {
state: Arc<Mutex<SinkState>>,
}
impl ShardWriter for CaptureWriter {
type Endpoint = ReplicaTag;
fn attach_metrics(&mut self, meter: Option<Meter>) {
if let Some(meter) = meter {
meter.counter("attaches_total", &[]).increment(1);
}
}
fn write_batch(
&self,
endpoint: &ReplicaTag,
batch: &SealedBatch,
) -> impl Future<Output = Result<(), SinkError>> + Send {
let state = Arc::clone(&self.state);
let endpoint = *endpoint;
let rows = batch.rows;
let bytes = batch.bytes;
let dedup_token = batch.dedup_token.clone();
let payload: Vec<u8> = batch
.frames
.iter()
.flat_map(|f: &Bytes| f.iter().copied())
.collect();
async move {
let outcome = {
let mut st = state.lock().expect("spate-test sink state poisoned");
let outcome = st
.per_endpoint
.get_mut(&(endpoint.shard, endpoint.replica))
.and_then(VecDeque::pop_front)
.or_else(|| st.global.pop_front())
.unwrap_or_else(WriteOutcome::ok);
st.writes.push(CapturedWrite {
shard: endpoint.shard,
replica: endpoint.replica,
rows,
bytes,
dedup_token,
payload,
result: outcome.result.clone(),
});
outcome
};
if let Some(delay) = outcome.delay {
tokio::time::sleep(delay).await;
}
match outcome.result {
ScriptedResult::Ok => Ok(()),
ScriptedResult::Retryable(reason) => Err(SinkError::Client {
class: ErrorClass::Retryable,
reason,
}),
ScriptedResult::Fatal(reason) => Err(SinkError::Client {
class: ErrorClass::Fatal,
reason,
}),
}
}
}
fn probe(&self, endpoint: &ReplicaTag) -> impl Future<Output = Result<(), SinkError>> + Send {
let state = Arc::clone(&self.state);
let endpoint = *endpoint;
async move {
let failure = state
.lock()
.expect("spate-test sink state poisoned")
.probe_failures
.get(&(endpoint.shard, endpoint.replica))
.cloned();
match failure {
None => Ok(()),
Some(reason) => Err(SinkError::Client {
class: ErrorClass::Retryable,
reason,
}),
}
}
}
}
#[must_use]
pub fn capture_sink(shards: usize, replicas: usize) -> (CaptureSink, SinkScript) {
assert!(shards > 0, "capture_sink needs at least one shard");
assert!(replicas > 0, "capture_sink needs at least one replica");
let (writer, script) = capture_writer();
(
CaptureSink {
writer,
shards,
replicas,
pool: SinkPoolConfig::default(),
},
script,
)
}
#[derive(Clone, Debug)]
pub struct CaptureSink {
writer: CaptureWriter,
shards: usize,
replicas: usize,
pool: SinkPoolConfig,
}
impl CaptureSink {
#[must_use]
pub fn with_pool_config(mut self, pool: SinkPoolConfig) -> Self {
self.pool = pool;
self
}
}
impl SinkBundle for CaptureSink {
type Writer = CaptureWriter;
fn into_parts(self) -> SinkParts<CaptureWriter> {
let endpoints: Vec<Vec<ReplicaTag>> = (0..self.shards)
.map(|shard| {
(0..self.replicas)
.map(|replica| ReplicaTag { shard, replica })
.collect()
})
.collect();
let probe = endpoint_probe(self.writer.clone(), Arc::new(endpoints.clone()));
SinkParts::new(self.writer, endpoints, self.pool)
.with_component_type("capture")
.with_probe(probe)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct TestEncoder;
impl RowEncoder<Owned<Vec<u8>>> for TestEncoder {
fn encode<'buf>(&mut self, rec: &Record<Vec<u8>>, buf: &mut BytesMut) -> Result<(), SinkError> {
let len = u32::try_from(rec.payload.len()).map_err(|_| SinkError::Client {
class: ErrorClass::RecordLevel,
reason: "row longer than u32::MAX".to_owned(),
})?;
buf.put_u32_le(len);
buf.extend_from_slice(&rec.payload);
Ok(())
}
}
#[must_use]
pub fn decode_rows(mut bytes: &[u8]) -> Vec<Vec<u8>> {
let mut rows = Vec::new();
while !bytes.is_empty() {
assert!(bytes.len() >= 4, "decode_rows: truncated length prefix");
let len = u32::from_le_bytes(bytes[..4].try_into().expect("4 bytes")) as usize;
bytes = &bytes[4..];
assert!(bytes.len() >= len, "decode_rows: truncated row body");
rows.push(bytes[..len].to_vec());
bytes = &bytes[len..];
}
rows
}