use std::thread;
use std::time::Duration;
use smallvec::SmallVec;
use tempfile::TempDir;
use tephra::Position;
use tephra::event::{Event, EventType, Tag, Tags};
use tephra::log::set::{SegmentConfig, SegmentSet};
use tephra::query::{Query, QueryItem};
use tephra::writer::{WriteCoordinator, WriteHandle, WriterConfig};
fn coordinator() -> (WriteCoordinator, WriteHandle, TempDir) {
let dir = TempDir::new().unwrap();
let set = SegmentSet::open(dir.path(), SegmentConfig::new(512)).unwrap();
let cfg = WriterConfig {
queue_capacity: 64,
max_batch_records: 64,
max_batch_bytes: 256,
tips_window: 1_000_000,
verify_tips: false,
..WriterConfig::default()
};
let (coord, handle) = WriteCoordinator::start(set, cfg).unwrap();
(coord, handle, dir)
}
fn tags(items: &[&str]) -> Tags {
Tags::new(
items
.iter()
.map(|s| Tag::new(*s).unwrap())
.collect::<SmallVec<[Tag; 4]>>(),
)
.unwrap()
}
fn tagged_event(ty: &str, tag_strs: &[&str]) -> Event {
Event::new(&EventType::new(ty).unwrap(), &tags(tag_strs), b"payload").unwrap()
}
fn append(handle: &WriteHandle, tag: &str) {
handle
.append(vec![tagged_event("E", &[tag])], None)
.unwrap();
}
fn tag_query(tag: &str) -> Query {
Query::item(QueryItem::with_tags(tags(&[tag])))
}
#[test]
fn catch_up_then_live_has_no_gap_or_duplicate() {
let (coord, handle, _dir) = coordinator();
let before = 60u64;
let after = 60u64;
let total = before + after;
for _ in 0..before {
append(&handle, "k:1");
}
let reader = handle.reader();
let subscriber = thread::spawn(move || {
let mut sub = reader.subscribe(Query::all(), Position::ZERO);
let mut positions = Vec::new();
while (positions.len() as u64) < total {
match sub.next_batch() {
Some(Ok(batch)) => positions.extend(batch.iter().map(|(p, _)| p.get())),
Some(Err(err)) => panic!("subscription error: {err}"),
None => break,
}
}
positions
});
for _ in 0..after {
append(&handle, "k:1");
}
let positions = subscriber.join().unwrap();
assert_eq!(
positions,
(1..=total).collect::<Vec<_>>(),
"delivered positions must be dense with no gap or duplicate"
);
coord.shutdown();
}
#[test]
fn subscription_under_contention_is_complete() {
let (coord, handle, _dir) = coordinator();
let total = 1000u64;
let reader = handle.reader();
let subscriber = thread::spawn(move || {
let mut sub = reader.subscribe(Query::all(), Position::ZERO);
let mut next_expected = 1u64;
while next_expected <= total {
match sub.next_batch() {
Some(Ok(batch)) => {
for (position, _) in batch {
assert_eq!(position.get(), next_expected, "gap or duplicate");
next_expected += 1;
}
}
Some(Err(err)) => panic!("subscription error: {err}"),
None => break,
}
}
next_expected - 1
});
for _ in 0..total {
append(&handle, "k:1");
}
assert_eq!(subscriber.join().unwrap(), total);
coord.shutdown();
}
#[test]
fn multiple_subscribers_each_receive_everything() {
let (coord, handle, _dir) = coordinator();
let total = 300u64;
let subscribers: Vec<_> = (0..4)
.map(|_| {
let reader = handle.reader();
thread::spawn(move || {
let mut sub = reader.subscribe(Query::all(), Position::ZERO);
let mut next_expected = 1u64;
while next_expected <= total {
match sub.next_batch() {
Some(Ok(batch)) => {
for (position, _) in batch {
assert_eq!(position.get(), next_expected, "gap or duplicate");
next_expected += 1;
}
}
Some(Err(err)) => panic!("subscription error: {err}"),
None => break,
}
}
next_expected - 1
})
})
.collect();
for _ in 0..total {
append(&handle, "k:1");
}
for subscriber in subscribers {
assert_eq!(subscriber.join().unwrap(), total);
}
coord.shutdown();
}
#[test]
fn selective_subscription_delivers_only_matches_and_advances_cursor() {
let (coord, handle, _dir) = coordinator();
for i in 0..20 {
append(&handle, if i % 2 == 0 { "k:hit" } else { "k:miss" });
}
let mut sub = handle.subscribe(tag_query("k:hit"), Position::ZERO);
let batch = sub.poll_batch().unwrap();
let got: Vec<u64> = batch.iter().map(|(p, _)| p.get()).collect();
let expected: Vec<u64> = (0u64..20).filter(|i| i % 2 == 0).map(|i| i + 1).collect();
assert_eq!(got, expected);
assert_eq!(sub.position(), Position::new(20));
assert!(sub.poll_batch().unwrap().is_empty());
coord.shutdown();
}
#[test]
fn subscribe_at_watermark_then_receives_next_append() {
let (coord, handle, _dir) = coordinator();
for _ in 0..10 {
append(&handle, "k:1");
}
let tip = handle.read(&Query::all(), Position::ZERO, None).watermark();
assert_eq!(tip, Position::new(10));
let mut sub = handle.subscribe(Query::all(), tip);
assert!(sub.poll_batch().unwrap().is_empty());
append(&handle, "k:1");
let batch = sub.next_batch().expect("store live").expect("no error");
assert_eq!(batch.len(), 1);
assert_eq!(batch[0].0, Position::new(11));
coord.shutdown();
}
#[test]
fn slow_subscriber_gets_bounded_batches_and_completeness() {
let (coord, handle, _dir) = coordinator();
for _ in 0..100 {
append(&handle, "k:1");
}
let mut sub = handle
.subscribe(Query::all(), Position::ZERO)
.with_max_batch_events(10);
let mut positions = Vec::new();
loop {
let batch = sub.poll_batch().unwrap();
if batch.is_empty() {
break;
}
assert!(batch.len() <= 10, "batch must respect the cap");
positions.extend(batch.iter().map(|(p, _)| p.get()));
}
assert_eq!(positions, (1..=100).collect::<Vec<_>>());
coord.shutdown();
}
#[test]
fn shutdown_wakes_a_blocked_subscriber() {
let (coord, handle, _dir) = coordinator();
append(&handle, "k:1");
let reader = handle.reader();
let subscriber = thread::spawn(move || {
let mut sub = reader.subscribe(Query::all(), Position::ZERO);
let first = sub.next_batch().expect("live").expect("no error");
assert_eq!(first.len(), 1);
sub.next_batch()
});
thread::sleep(Duration::from_millis(100));
coord.shutdown();
assert!(
subscriber.join().unwrap().is_none(),
"a blocked subscriber must observe shutdown and end"
);
}
#[test]
fn wait_timeout_ticks_then_advances() {
use tephra::WaitOutcome;
let (coord, handle, _dir) = coordinator();
append(&handle, "k:1");
let mut sub = handle.subscribe(Query::all(), Position::ZERO);
assert_eq!(sub.poll_batch().unwrap().len(), 1);
assert!(sub.poll_batch().unwrap().is_empty());
assert_eq!(
sub.wait_timeout(Duration::from_millis(20)),
WaitOutcome::TimedOut
);
append(&handle, "k:1");
assert_eq!(
sub.wait_timeout(Duration::from_millis(500)),
WaitOutcome::Advanced
);
coord.shutdown();
}
#[cfg(feature = "async")]
#[test]
fn next_batch_async_catches_up_then_tails_then_closes() {
use std::future::Future;
use std::sync::Arc;
use std::task::{Context, Poll, Wake, Waker};
fn block_on<F: Future>(fut: F) -> F::Output {
use std::pin::pin;
struct ThreadWaker(thread::Thread);
impl Wake for ThreadWaker {
fn wake(self: Arc<Self>) {
self.0.unpark();
}
fn wake_by_ref(self: &Arc<Self>) {
self.0.unpark();
}
}
let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
let mut cx = Context::from_waker(&waker);
let mut fut = pin!(fut);
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(out) => return out,
Poll::Pending => thread::park(),
}
}
}
let (coord, handle, _dir) = coordinator();
let before = 40u64;
let after = 40u64;
let total = before + after;
for _ in 0..before {
append(&handle, "k:1");
}
let mut sub = handle.subscribe(Query::all(), Position::ZERO);
let writer = {
let handle = handle.clone();
thread::spawn(move || {
for _ in 0..after {
append(&handle, "k:1");
thread::sleep(Duration::from_millis(1));
}
})
};
let positions = block_on(async {
let mut out: Vec<u64> = Vec::new();
while (out.len() as u64) < total {
match sub.next_batch_async().await {
Some(Ok(batch)) => out.extend(batch.into_iter().map(|(p, _)| p.get())),
Some(Err(err)) => panic!("read error: {err}"),
None => break,
}
}
out
});
writer.join().unwrap();
assert_eq!(positions, (1..=total).collect::<Vec<_>>());
coord.shutdown();
assert!(
block_on(sub.next_batch_async()).is_none(),
"a closed store ends the async stream"
);
}
#[test]
fn shutdown_delivers_a_final_batch_committed_while_parked() {
let (coord, handle, _dir) = coordinator();
append(&handle, "k:1"); let mut sub = handle.subscribe(Query::all(), Position::ZERO);
assert_eq!(sub.poll_batch().unwrap().len(), 1); assert!(sub.poll_batch().unwrap().is_empty());
let sub_thread = thread::spawn(move || sub.next_batch());
thread::sleep(Duration::from_millis(50)); append(&handle, "k:1"); coord.shutdown();
let batch = sub_thread
.join()
.unwrap()
.expect("the final batch is delivered, not dropped at close")
.unwrap();
assert_eq!(
batch.iter().map(|(p, _)| p.get()).collect::<Vec<_>>(),
vec![2],
);
}