use bytes::{BufMut, Bytes, BytesMut};
use spate_core::deser::Owned;
use spate_core::error::{ErrorClass, SinkError};
use spate_core::hash::FastMap;
use spate_core::record::Record;
use spate_core::sink::{
FrameCodecFactory, RowEncoder, SealedBatch, ShardWriter, SinkBundle, SinkMetricsCtx, SinkParts,
SinkPoolConfig, endpoint_probe,
};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex, MutexGuard};
use std::task::{Poll, Waker};
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),
Pending,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct WriteOutcome {
pub hold: bool,
pub delay: Option<Duration>,
pub result: ScriptedResult,
}
impl WriteOutcome {
#[must_use]
pub fn ok() -> Self {
WriteOutcome {
hold: false,
delay: None,
result: ScriptedResult::Ok,
}
}
#[must_use]
pub fn retryable(reason: impl Into<String>) -> Self {
WriteOutcome {
hold: false,
delay: None,
result: ScriptedResult::Retryable(reason.into()),
}
}
#[must_use]
pub fn fatal(reason: impl Into<String>) -> Self {
WriteOutcome {
hold: false,
delay: None,
result: ScriptedResult::Fatal(reason.into()),
}
}
#[must_use]
pub fn after(mut self, delay: Duration) -> Self {
self.delay = Some(delay);
self
}
#[must_use]
pub fn held(mut self) -> Self {
self.hold = true;
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: FastMap<(usize, usize), VecDeque<WriteOutcome>>,
probe_failures: FastMap<(usize, usize), String>,
writes: Vec<CapturedWrite>,
releases: u64,
holding: usize,
held_wakers: Vec<Waker>,
}
struct Holding(Arc<Mutex<SinkState>>);
impl Drop for Holding {
fn drop(&mut self) {
if let Ok(mut st) = self.0.lock() {
st.holding -= 1;
}
}
}
fn refuse_pending(outcome: &WriteOutcome) {
assert!(
outcome.result != ScriptedResult::Pending,
"a write cannot be scripted to resolve as Pending; script it with WriteOutcome::held()"
);
}
#[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) {
refuse_pending(&outcome);
self.lock().global.push_back(outcome);
}
pub fn enqueue_for(&self, shard: usize, replica: usize, outcome: WriteOutcome) {
refuse_pending(&outcome);
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()
}
pub fn release_held(&self) {
let wakers = {
let mut st = self.lock();
st.releases += 1;
std::mem::take(&mut st.held_wakers)
};
for waker in wakers {
waker.wake();
}
}
#[must_use]
pub fn held_writes(&self) -> usize {
self.lock().holding
}
}
#[derive(Clone, Debug)]
pub struct CaptureWriter {
state: Arc<Mutex<SinkState>>,
}
impl ShardWriter for CaptureWriter {
type Endpoint = ReplicaTag;
fn attach_metrics(&mut self, cx: SinkMetricsCtx) {
if let Some(meter) = cx.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, hold, at) = {
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);
let at = st.writes.len();
st.writes.push(CapturedWrite {
shard: endpoint.shard,
replica: endpoint.replica,
rows,
bytes,
dedup_token,
payload,
result: ScriptedResult::Pending,
});
let hold = outcome.hold.then(|| {
st.holding += 1;
(st.releases, Holding(Arc::clone(&state)))
});
(outcome, hold, at)
};
if let Some((started_at, _holding)) = hold {
std::future::poll_fn(|cx| {
let mut st = state.lock().expect("spate-test sink state poisoned");
if st.releases > started_at {
Poll::Ready(())
} else {
st.held_wakers.push(cx.waker().clone());
Poll::Pending
}
})
.await;
}
if let Some(delay) = outcome.delay {
tokio::time::sleep(delay).await;
}
let resolved = match &outcome.result {
ScriptedResult::Ok => Ok(()),
ScriptedResult::Retryable(reason) => Err(SinkError::Client {
class: ErrorClass::Retryable,
reason: reason.clone(),
}),
ScriptedResult::Fatal(reason) => Err(SinkError::Client {
class: ErrorClass::Fatal,
reason: reason.clone(),
}),
ScriptedResult::Pending => {
unreachable!("the enqueue methods refuse a Pending outcome")
}
};
state.lock().expect("spate-test sink state poisoned").writes[at].result =
outcome.result;
resolved
}
}
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(),
codec: None,
},
script,
)
}
#[derive(Clone, Debug)]
pub struct CaptureSink {
writer: CaptureWriter,
shards: usize,
replicas: usize,
pool: SinkPoolConfig,
codec: Option<FrameCodecFactory>,
}
impl CaptureSink {
#[must_use]
pub fn with_pool_config(mut self, pool: SinkPoolConfig) -> Self {
self.pool = pool;
self
}
#[must_use]
pub fn with_frame_codec(mut self, codec: FrameCodecFactory) -> Self {
self.codec = Some(codec);
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()));
let parts = SinkParts::new(self.writer, endpoints, self.pool)
.with_component_type("capture")
.with_probe(probe);
match self.codec {
Some(codec) => parts.with_frame_codec(codec),
None => parts,
}
}
}
#[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
}