#![allow(dead_code)]
pub(crate) mod seaweed;
pub(crate) mod spy;
use spate_coordination::store::memory::MemoryStore;
use spate_coordination::{CoordinationConfig, StoreCoordinator};
use spate_core::config::PipelineConfig;
use spate_core::framing::RecordFramer;
use spate_core::ops::chain_owned;
use spate_core::pipeline::{Pipeline, RuntimeOptions, ShutdownHandle};
use spate_core::sink::KeyHashRouter;
use spate_s3::S3Source;
use spate_test::{BytesPassthrough, PipelineRun, SinkScript, TestEncoder, capture_sink};
use std::collections::VecDeque;
use std::io;
use std::time::Duration;
#[derive(Default)]
pub(crate) struct LineFramer {
partial: Vec<u8>,
ready: VecDeque<Vec<u8>>,
decoded: u64,
}
impl RecordFramer for LineFramer {
fn push(&mut self, bytes: &[u8]) -> io::Result<()> {
self.decoded += bytes.len() as u64;
for &b in bytes {
if b == b'\n' {
self.flush_line();
} else {
self.partial.push(b);
}
}
Ok(())
}
fn finish(&mut self) -> io::Result<()> {
if !self.partial.is_empty() {
self.flush_line();
}
Ok(())
}
fn pop(&mut self) -> Option<Vec<u8>> {
self.ready.pop_front()
}
fn decoded_bytes(&self) -> u64 {
self.decoded
}
}
impl LineFramer {
fn flush_line(&mut self) {
if self.partial.last() == Some(&b'\r') {
self.partial.pop();
}
if self.partial.iter().all(u8::is_ascii_whitespace) {
self.partial.clear();
return;
}
self.ready.push_back(std::mem::take(&mut self.partial));
}
}
pub(crate) fn line_framer(source: S3Source) -> S3Source {
source.with_framer(|| Box::new(LineFramer::default()))
}
pub(crate) struct Launched {
pub(crate) run: PipelineRun,
pub(crate) script: SinkScript,
pub(crate) shutdown: ShutdownHandle,
}
pub(crate) fn launch_scripted(
yaml: &str,
options: RuntimeOptions,
pre: impl FnOnce(&SinkScript),
) -> Launched {
launch_customized(yaml, options, pre, |source, _io| line_framer(source))
}
pub(crate) const TEST_LEASE: Duration = Duration::from_secs(1);
pub(crate) fn test_tuning() -> CoordinationConfig {
CoordinationConfig {
lease_duration: TEST_LEASE,
op_timeout: Duration::from_millis(200),
replan_interval: TEST_LEASE,
reconcile_interval: Duration::from_millis(300),
rebalance_delay: Duration::ZERO,
drain_deadline: Duration::from_secs(5),
..CoordinationConfig::default()
}
}
pub(crate) fn shared_store() -> MemoryStore {
MemoryStore::new(TEST_LEASE)
}
pub(crate) fn launch_on_store(
yaml: &str,
options: RuntimeOptions,
store: &MemoryStore,
pre: impl FnOnce(&SinkScript),
) -> Launched {
launch_tuned(yaml, options, store, test_tuning(), pre)
}
pub(crate) fn launch_tuned(
yaml: &str,
options: RuntimeOptions,
store: &MemoryStore,
tuning: CoordinationConfig,
pre: impl FnOnce(&SinkScript),
) -> Launched {
let store = store.clone();
launch_customized(yaml, options, pre, move |source, io| {
let coordinator =
StoreCoordinator::new(store, tuning, io, None).expect("coordinator builds");
line_framer(source).with_coordinator(Box::new(coordinator))
})
}
fn next_launch() -> u64 {
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
}
pub(crate) fn launch_customized(
yaml: &str,
options: RuntimeOptions,
pre: impl FnOnce(&SinkScript),
make_source: impl FnOnce(S3Source, tokio::runtime::Handle) -> S3Source,
) -> Launched {
let mut config = PipelineConfig::from_str(yaml).expect("config parses");
config.pipeline.name = format!("{}-{}", config.pipeline.name, next_launch());
let pipeline = Pipeline::from_config(config).expect("pipeline builds");
let io = pipeline.io_handle();
let source = make_source(
S3Source::from_component_config(&pipeline.config().source, io.clone())
.expect("source config"),
io,
);
let (sink, script) = capture_sink(1, 1);
pre(&script);
let runtime = pipeline
.sink(sink)
.expect("sink installs")
.chains(|ctx| {
let chunk_cfg = ctx.chunk();
chain_owned::<Vec<u8>, _>(BytesPassthrough)
.sink(
TestEncoder,
KeyHashRouter,
chunk_cfg,
ctx.queues,
ctx.budget,
)
.build()
})
.runtime_options(options)
.into_runtime(source)
.expect("runtime assembles");
let shutdown = runtime.shutdown_handle();
let run = PipelineRun::spawn(move || runtime.run());
Launched {
run,
script,
shutdown,
}
}
pub(crate) fn launch(yaml: &str, options: RuntimeOptions) -> Launched {
launch_scripted(yaml, options, |_| {})
}
pub(crate) fn test_options() -> RuntimeOptions {
RuntimeOptions {
handle_signals: false,
..RuntimeOptions::default()
}
}
pub(crate) fn captured_rows(script: &SinkScript) -> Vec<String> {
script
.writes()
.iter()
.flat_map(|w| spate_test::decode_rows(&w.payload))
.map(|r| String::from_utf8(r).unwrap())
.collect()
}
pub(crate) fn sorted(mut v: Vec<String>) -> Vec<String> {
v.sort();
v
}
pub(crate) fn recs(prefix: &str, n: usize) -> Vec<String> {
(0..n)
.map(|i| format!("{{\"k\":\"{prefix}-{i}\"}}"))
.collect()
}
pub(crate) fn lines_bytes(lines: &[String]) -> Vec<u8> {
let mut out = Vec::new();
for l in lines {
out.extend_from_slice(l.as_bytes());
out.push(b'\n');
}
out
}