#![cfg(velo_endurance)]
mod common;
use std::sync::Arc;
use std::time::Duration;
use futures::StreamExt;
use velo::streaming::{AnchorManager, StreamController, StreamFrame};
use velo_ext::WorkerId;
use common::MockFrameTransport;
fn make_manager() -> Arc<AnchorManager> {
Arc::new(AnchorManager::new(
WorkerId::from_u64(1),
Arc::new(MockFrameTransport::new()),
))
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e01_sustained_10k_frames() {
const TOTAL: u64 = 10_000;
let mgr = make_manager();
let mut anchor = mgr.create_anchor::<u64>();
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u64>(handle)
.await
.expect("attach");
let send_task = tokio::spawn(async move {
for i in 0..TOTAL {
sender.send(i).await.expect("send");
}
sender.finalize().expect("finalize");
});
let mut received: Vec<u64> = Vec::with_capacity(TOTAL as usize);
while let Some(frame) = anchor.next().await {
match frame {
Ok(StreamFrame::Item(v)) => received.push(v),
Ok(StreamFrame::Finalized) => break,
other => panic!("unexpected: {other:?}"),
}
}
send_task.await.expect("send task");
assert_eq!(received.len(), TOTAL as usize, "zero frame loss");
assert_eq!(
received,
(0..TOTAL).collect::<Vec<_>>(),
"frames must be in order"
);
assert!(anchor.next().await.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e02_concurrent_100_streams() {
const STREAMS: usize = 100;
const ITEMS: u32 = 100;
let mgr = make_manager();
let mut anchors: Vec<_> = Vec::new();
let mut senders: Vec<_> = Vec::new();
for _ in 0..STREAMS {
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u32>(handle)
.await
.expect("attach");
anchors.push(anchor);
senders.push(sender);
}
let send_tasks: Vec<_> = senders
.into_iter()
.enumerate()
.map(|(idx, sender)| {
tokio::spawn(async move {
let base = idx as u32 * 1000;
for j in 0..ITEMS {
sender.send(base + j).await.expect("send");
}
sender.finalize().expect("finalize");
})
})
.collect();
let recv_tasks: Vec<_> = anchors
.into_iter()
.enumerate()
.map(|(idx, mut anchor)| {
tokio::spawn(async move {
let base = idx as u32 * 1000;
let mut got: Vec<u32> = Vec::new();
while let Some(frame) = anchor.next().await {
match frame {
Ok(StreamFrame::Item(v)) => got.push(v),
Ok(StreamFrame::Finalized) => break,
other => panic!("stream {idx}: unexpected {other:?}"),
}
}
(idx, got, base)
})
})
.collect();
for t in send_tasks {
t.await.expect("send task");
}
for t in recv_tasks {
let (idx, got, base) = t.await.expect("recv task");
let expected: Vec<u32> = (0..ITEMS).map(|j| base + j).collect();
assert_eq!(got, expected, "stream {idx}: received wrong items");
}
assert_eq!(
mgr.active_anchor_count(),
0,
"all anchors must be removed after finalize"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e03_rapid_create_destroy_anchors() {
const ROUNDS: usize = 1_000;
let mgr = make_manager();
for round in 0..ROUNDS {
let mut anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u32>(handle)
.await
.unwrap_or_else(|e| panic!("round {round} attach: {e}"));
sender
.finalize()
.unwrap_or_else(|e| panic!("round {round} finalize: {e}"));
while let Some(f) = anchor.next().await {
if matches!(f, Ok(StreamFrame::Finalized)) {
break;
}
}
assert!(anchor.next().await.is_none());
assert_eq!(
mgr.active_anchor_count(),
0,
"round {round}: registry must be empty after finalize"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e04_sender_panic_drop() {
use velo::streaming::StreamError;
let mgr = make_manager();
let mut anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u32>(handle)
.await
.expect("attach");
let send_task = tokio::spawn(async move {
for i in 0u32..500 {
sender.send(i).await.expect("send");
}
panic!("deliberate sender panic for E-04");
});
let mut items: Vec<u32> = Vec::new();
let mut saw_dropped = false;
while let Some(frame) = anchor.next().await {
match frame {
Ok(StreamFrame::Item(v)) => items.push(v),
Err(StreamError::SenderDropped) => {
saw_dropped = true;
break;
}
other => panic!("unexpected: {other:?}"),
}
}
let _ = send_task.await;
assert_eq!(items.len(), 500, "must receive all 500 items before panic");
assert!(saw_dropped, "Dropped sentinel must arrive after panic-drop");
assert!(anchor.next().await.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e05_consumer_slow_sustained() {
const TOTAL: u32 = 1_000;
let mgr = make_manager();
let mut anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u32>(handle)
.await
.expect("attach");
let send_task = tokio::spawn(async move {
for i in 0..TOTAL {
sender.send(i).await.expect("send");
}
sender.finalize().expect("finalize");
});
let mut items: Vec<u32> = Vec::new();
while let Some(frame) = anchor.next().await {
match frame {
Ok(StreamFrame::Item(v)) => {
items.push(v);
tokio::time::sleep(Duration::from_millis(1)).await;
}
Ok(StreamFrame::Finalized) => break,
other => panic!("unexpected: {other:?}"),
}
}
send_task.await.expect("send task");
assert_eq!(
items.len(),
TOTAL as usize,
"zero frame loss under slow consumer"
);
assert_eq!(items, (0..TOTAL).collect::<Vec<_>>(), "items in order");
assert!(anchor.next().await.is_none());
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e06_five_cycle_reattach_endurance() {
const CYCLES: u32 = 5;
const BATCH: u32 = 100;
let mgr = make_manager();
let mut anchor = mgr.create_anchor::<u32>();
let mut current_handle = anchor.handle();
let mut all_items: Vec<u32> = Vec::new();
let mut detached_count: u32 = 0;
let mut offset: u32 = 0;
for cycle in 0..CYCLES {
let sender = mgr
.attach_stream_anchor::<u32>(current_handle)
.await
.unwrap_or_else(|e| panic!("cycle {cycle} attach: {e}"));
for j in 0..BATCH {
sender.send(offset + j).await.expect("send");
}
offset += BATCH;
current_handle = sender.detach().expect("detach");
loop {
let f = tokio::time::timeout(Duration::from_secs(10), anchor.next())
.await
.expect("timeout")
.expect("stream open");
match f {
Ok(StreamFrame::Item(v)) => all_items.push(v),
Ok(StreamFrame::Detached) => {
detached_count += 1;
break;
}
other => panic!("cycle {cycle}: unexpected {other:?}"),
}
}
}
let last_sender = mgr
.attach_stream_anchor::<u32>(current_handle)
.await
.expect("final attach");
for j in 0..BATCH {
last_sender.send(offset + j).await.expect("send");
}
last_sender.finalize().expect("finalize");
loop {
let f = tokio::time::timeout(Duration::from_secs(10), anchor.next())
.await
.expect("timeout")
.expect("stream open");
match f {
Ok(StreamFrame::Item(v)) => all_items.push(v),
Ok(StreamFrame::Finalized) => break,
other => panic!("final batch: unexpected {other:?}"),
}
}
assert!(anchor.next().await.is_none());
let total = (CYCLES + 1) * BATCH;
assert_eq!(
all_items.len(),
total as usize,
"must receive all {total} items"
);
assert_eq!(
all_items,
(0..total).collect::<Vec<_>>(),
"items must be in strict order"
);
assert_eq!(
detached_count, CYCLES,
"must see exactly {CYCLES} Detached sentinels"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_e07_cancel_storm() {
const STORMERS: usize = 50;
let mgr = make_manager();
let anchor = mgr.create_anchor::<u32>();
let ctrl: StreamController = anchor.controller();
let tasks: Vec<_> = (0..STORMERS)
.map(|_| {
let c = ctrl.clone();
tokio::spawn(async move { c.cancel() })
})
.collect();
for t in tasks {
t.await.expect("task");
}
assert_eq!(
mgr.active_anchor_count(),
0,
"registry must be empty after cancel storm"
);
}