use super::*;
use crate::pipeline::ProcessError;
use crate::primitives::sync::atomic::{AtomicUsize, Ordering};
use std::future::Future;
use std::io::Write;
use std::pin::Pin;
struct CountingProcessor(Arc<AtomicUsize>);
impl SegmentProcessor for CountingProcessor {
fn name(&self) -> &'static str {
"CountingProcessor"
}
fn process(
&mut self,
data: SegmentData,
) -> Pin<Box<dyn Future<Output = Result<SegmentData, ProcessError>> + Send + '_>> {
Box::pin(async move {
self.0.fetch_add(1, Ordering::Relaxed);
Ok(data)
})
}
}
const WRITERS: usize = 3;
const SEGMENTS_PER_WRITER: u32 = 2;
const FIXED_EPOCH_SECS_FOR_TEST: u64 = 1_000;
fn segment_bytes_with_epoch(epoch_secs: u64) -> Vec<u8> {
use dial9_trace_format::encoder::Encoder;
let mut enc = Encoder::new_to(Vec::new()).unwrap();
enc.write_infallible(&crate::format::ClockSyncEvent {
timestamp_ns: 1,
realtime_ns: epoch_secs * 1_000_000_000,
});
enc.into_inner()
}
fn spawn_writers(fs: Arc<Fs>) -> Vec<crate::primitives::thread::JoinHandle<()>> {
(0..WRITERS)
.map(|w| {
let fs = fs.clone();
crate::primitives::thread::spawn(move || {
for i in 0..SEGMENTS_PER_WRITER {
let index = w as u32 * SEGMENTS_PER_WRITER + i;
let mut h = fs.create_segment(Path::new("trace")).unwrap();
h.write_all(&segment_bytes_with_epoch(FIXED_EPOCH_SECS_FOR_TEST))
.unwrap();
fs.seal(h, Path::new("trace"), index).unwrap();
fs.set_seal_secs_for_test(index, FIXED_EPOCH_SECS_FOR_TEST);
}
})
})
.collect()
}
crate::shuttle_test! {
num_iters = 2_000, depth = 3;
fn shuttle_worker_handoff() {
let fs = Fs::new_in_memory(1 << 20, 4096).unwrap();
let processed = Arc::new(AtomicUsize::new(0));
let writers = spawn_writers(fs.clone());
let worker_fs = fs.clone();
let worker_processed = processed.clone();
let worker = crate::primitives::thread::spawn(move || {
shuttle::future::block_on(async move {
let stop = tokio_util::sync::CancellationToken::new();
let mut worker = WorkerLoop::new(
worker_fs.clone(),
Duration::from_millis(1),
vec![Box::new(CountingProcessor(worker_processed))],
stop,
metrique::writer::sink::DevNullSink::boxed(),
None,
)
.await
.expect("initialize worker");
worker.run().await;
});
});
for w in writers {
w.join().unwrap();
}
fs.mark_writer_done();
worker.join().unwrap();
assert_eq!(
processed.load(Ordering::Relaxed),
WRITERS * SEGMENTS_PER_WRITER as usize,
"every sealed segment must be processed exactly once"
);
}
}
crate::shuttle_test! {
num_iters = 500, depth = 3;
fn shuttle_dump_resolves_exactly_once() {
let fs = Fs::new_in_memory(1 << 20, 4096).unwrap();
let processed = Arc::new(AtomicUsize::new(0));
let writers = spawn_writers(fs.clone());
let (trigger, rx) = crate::dump::channel();
let trigger_handle = crate::primitives::thread::spawn(move || {
shuttle::future::block_on(async move {
let receipt = trigger.dump_current_data().await;
assert!(
receipt.is_ok(),
"an on-demand dump request must resolve successfully: {receipt:?}"
);
});
});
let stop = tokio_util::sync::CancellationToken::new();
let worker_fs = fs.clone();
let worker_processed = processed.clone();
let worker_stop = stop.clone();
let worker = crate::primitives::thread::spawn(move || {
shuttle::future::block_on(async move {
let mut worker = WorkerLoop::new(
worker_fs.clone(),
Duration::from_millis(1),
vec![Box::new(CountingProcessor(worker_processed))],
worker_stop,
metrique::writer::sink::DevNullSink::boxed(),
Some(rx),
)
.await
.expect("initialize worker");
let rx = worker.trigger.take().expect("triggered mode");
worker.run_triggered(rx).await;
});
});
for w in writers {
w.join().unwrap();
}
trigger_handle.join().unwrap();
fs.mark_writer_done();
stop.cancel();
worker.join().unwrap();
assert!(
processed.load(Ordering::Relaxed) <= WRITERS * SEGMENTS_PER_WRITER as usize,
"dump must not double-dispatch a segment to the processor"
);
}
}
const LOOKFORWARD: Duration = Duration::from_millis(1);
mod shuttle_dump_time_range_resolves_via_deadline {
use super::*;
fn shuttle_dump_time_range_resolves_via_deadline() {
let fs = Fs::new_in_memory(1 << 20, 4096).unwrap();
let processed = Arc::new(AtomicUsize::new(0));
let writers = spawn_writers(fs.clone());
let (trigger, rx) = crate::dump::channel();
let trigger_handle = crate::primitives::thread::spawn(move || {
shuttle::future::block_on(async move {
let receipt = trigger.dump_time_range(Duration::MAX, LOOKFORWARD).await;
assert!(
receipt.is_ok(),
"a windowed dump request must resolve successfully: {receipt:?}"
);
});
});
let stop = tokio_util::sync::CancellationToken::new();
let worker_fs = fs.clone();
let worker_processed = processed.clone();
let worker_stop = stop.clone();
let worker = crate::primitives::thread::spawn(move || {
shuttle::future::block_on(async move {
let mut worker = WorkerLoop::new(
worker_fs.clone(),
Duration::from_millis(1),
vec![Box::new(CountingProcessor(worker_processed))],
worker_stop,
metrique::writer::sink::DevNullSink::boxed(),
Some(rx),
)
.await
.expect("initialize worker");
let rx = worker.trigger.take().expect("triggered mode");
worker.run_triggered(rx).await;
});
});
for w in writers {
w.join().unwrap();
}
trigger_handle.join().unwrap();
fs.mark_writer_done();
stop.cancel();
worker.join().unwrap();
assert!(
processed.load(Ordering::Relaxed) <= WRITERS * SEGMENTS_PER_WRITER as usize,
"dump must not double-dispatch a segment to the processor"
);
}
fn bumped_stack_config() -> shuttle::Config {
let mut config = shuttle::Config::new();
config.stack_size = 0x000F_0000;
config
}
#[test]
fn pct() {
crate::primitives::time::take_yield_pending_polls();
{
use shuttle::scheduler::PctScheduler;
let scheduler = PctScheduler::new(3, 500);
let runner = shuttle::Runner::new(scheduler, bumped_stack_config());
runner.run(shuttle_dump_time_range_resolves_via_deadline);
}
assert!(
crate::primitives::time::take_yield_pending_polls() > 0,
"no sleep/sleep_until await ever suspended: this scenario never \
exercised the deadline path it exists to cover"
);
}
#[test]
fn determinism() {
use shuttle::scheduler::{RandomScheduler, UncontrolledNondeterminismCheckScheduler};
let scheduler = UncontrolledNondeterminismCheckScheduler::new(RandomScheduler::new(500));
let runner = shuttle::Runner::new(scheduler, bumped_stack_config());
runner.run(shuttle_dump_time_range_resolves_via_deadline);
}
}
struct PanickingInitializer;
impl SegmentProcessor for PanickingInitializer {
fn name(&self) -> &'static str {
"PanickingInitializer"
}
fn initialize(&mut self) -> Pin<Box<dyn Future<Output = std::io::Result<()>> + Send + '_>> {
Box::pin(async { panic!("PanickingInitializer: simulated init panic") })
}
fn process(
&mut self,
data: SegmentData,
) -> Pin<Box<dyn Future<Output = Result<SegmentData, ProcessError>> + Send + '_>> {
Box::pin(async move { Ok(data) })
}
}
crate::shuttle_test! {
num_iters = 500, determinism_only;
fn shuttle_background_task_contains_init_panic() {
let fs = Fs::new_in_memory(1 << 20, 4096).unwrap();
let config = BackgroundTaskConfig::builder()
.processors(vec![Box::new(PanickingInitializer) as Box<dyn SegmentProcessor>])
.build();
let (_shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
let worker = crate::primitives::thread::spawn(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
shuttle::future::block_on(run_background_task_inner(config, shutdown_rx, fs));
}))
});
assert!(
worker.join().unwrap().is_ok(),
"a panic inside WorkerLoop::new/run must be caught by \
run_background_task_inner's top-level catch_unwind, not \
propagate to its caller"
);
}
}