use crate::observability::{HandlerOutcome, StreamingOp};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::streaming::anchor::AnchorManager;
use crate::streaming::handle::StreamAnchorHandle;
pub const DETECTION_MULTIPLIER: u8 = 3;
fn default_heartbeat_interval_ms() -> u64 {
5_000
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StreamCancelHandle(u128);
#[derive(Serialize, Deserialize)]
struct StreamCancelHandleWire {
hi: u64,
lo: u64,
}
impl StreamCancelHandle {
pub fn pack(worker_id: velo_ext::WorkerId, stream_id: u64) -> Self {
Self(((worker_id.as_u64() as u128) << 64) | (stream_id as u128))
}
pub fn unpack(self) -> (velo_ext::WorkerId, u64) {
let hi = (self.0 >> 64) as u64;
let lo = self.0 as u64;
(velo_ext::WorkerId::from_u64(hi), lo)
}
}
impl Serialize for StreamCancelHandle {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
StreamCancelHandleWire {
hi: (self.0 >> 64) as u64,
lo: self.0 as u64,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for StreamCancelHandle {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = StreamCancelHandleWire::deserialize(deserializer)?;
Ok(Self(((wire.hi as u128) << 64) | (wire.lo as u128)))
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StreamCancelRequest {
pub sender_stream_id: u64,
}
pub struct SenderEntry {
pub cancel_token: tokio_util::sync::CancellationToken,
pub rx_closer: std::sync::Mutex<Option<flume::Receiver<()>>>,
}
#[derive(Default)]
pub struct SenderRegistry {
pub senders: dashmap::DashMap<u64, SenderEntry>,
}
pub fn create_stream_cancel_handler(
sender_registry: Arc<SenderRegistry>,
) -> crate::messenger::Handler {
crate::messenger::Handler::am_handler(
"_stream_cancel",
move |ctx: crate::messenger::Context| {
let req = serde_json::from_slice::<StreamCancelRequest>(&ctx.payload)?;
if let Some((_, entry)) = sender_registry.senders.remove(&req.sender_stream_id) {
drop(entry.rx_closer.lock().unwrap().take());
entry.cancel_token.cancel();
}
Ok(())
},
)
.build()
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnchorAttachRequest {
pub handle: StreamAnchorHandle,
pub session_id: u64,
pub stream_cancel_handle: StreamCancelHandle,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum AnchorAttachResponse {
Ok {
stream_endpoint: String,
#[serde(default = "default_heartbeat_interval_ms")]
heartbeat_interval_ms: u64,
},
Err { reason: String },
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnchorDetachRequest {
pub handle: StreamAnchorHandle,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnchorFinalizeRequest {
pub handle: StreamAnchorHandle,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AnchorCancelRequest {
pub handle: StreamAnchorHandle,
}
pub(crate) async fn reader_pump(
transport_rx: flume::Receiver<Vec<u8>>,
frame_tx: flume::Sender<Vec<u8>>,
cancel_token: tokio_util::sync::CancellationToken,
ctx: crate::streaming::anchor::AnchorContext,
local_id: u64,
heartbeat_deadline: Duration,
) {
let crate::streaming::anchor::AnchorContext {
registry,
mpsc_registry,
metrics,
} = ctx;
let mut missed_heartbeats: u8 = 0;
loop {
tokio::select! {
_ = cancel_token.cancelled() => break,
result = tokio::time::timeout(heartbeat_deadline, transport_rx.recv_async()) => {
match result {
Ok(Ok(bytes)) => {
missed_heartbeats = 0;
if frame_tx.send_async(bytes).await.is_err() {
break; }
}
Ok(Err(_)) => break, Err(_timeout) => {
missed_heartbeats += 1;
if missed_heartbeats >= DETECTION_MULTIPLIER {
let dropped_bytes = crate::streaming::sender::cached_dropped().clone();
let _ = frame_tx.send_async(dropped_bytes).await;
if let Some((_, entry)) = registry.remove(&local_id) {
entry.cancel_token.cancel();
crate::streaming::anchor::set_active_anchor_gauge(
metrics.as_ref(),
®istry,
&mpsc_registry,
);
}
break;
}
}
}
}
}
}
cancel_token.cancel();
}
pub fn create_anchor_attach_handler(manager: Arc<AnchorManager>) -> crate::messenger::Handler {
crate::messenger::Handler::typed_unary_async(
"_anchor_attach",
move |ctx: crate::messenger::TypedContext<AnchorAttachRequest>| {
let manager = manager.clone();
async move {
let started = Instant::now();
let req = ctx.input;
if req.handle.is_mpsc_stream() {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
return Ok(AnchorAttachResponse::Err {
reason: format!("anchor {} is mpsc; use _mpsc_anchor_attach", req.handle),
});
}
let (_, local_id) = req.handle.unpack();
{
let entry = manager.registry.get(&local_id);
match entry {
None => {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
return Ok(AnchorAttachResponse::Err {
reason: format!("anchor {} not found", req.handle),
});
}
Some(e) if e.attachment => {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
return Ok(AnchorAttachResponse::Err {
reason: format!("anchor {} already attached", req.handle),
});
}
_ => {} }
}
let (endpoint, receiver) =
match manager.transport.bind(local_id, req.session_id).await {
Ok(pair) => pair,
Err(e) => {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
return Ok(AnchorAttachResponse::Err {
reason: format!("transport error: {}", e),
});
}
};
use dashmap::mapref::entry::Entry;
match manager.registry.entry(local_id) {
Entry::Vacant(_) => {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
Ok(AnchorAttachResponse::Err {
reason: format!("anchor {} removed during bind", req.handle),
})
}
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if entry.attachment {
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Error,
"unknown",
started,
);
Ok(AnchorAttachResponse::Err {
reason: format!("anchor {} already attached", req.handle),
})
} else {
let pump_cancel = entry.cancel_token.child_token();
entry.active_pump_token = Some(pump_cancel.clone());
let pump_frame_tx = entry.frame_tx.clone();
let heartbeat_interval = entry.heartbeat_interval;
entry.attachment = true;
entry.stream_cancel_handle = Some(req.stream_cancel_handle);
drop(occ);
let (_, local_id) = req.handle.unpack();
tokio::spawn(reader_pump(
receiver, pump_frame_tx, pump_cancel, manager.anchor_context(),
local_id, heartbeat_interval,
));
let transport_scheme =
endpoint.split("://").next().unwrap_or("unknown");
manager.record_streaming_operation(
StreamingOp::Attach,
HandlerOutcome::Success,
transport_scheme,
started,
);
Ok(AnchorAttachResponse::Ok {
stream_endpoint: endpoint,
heartbeat_interval_ms: heartbeat_interval.as_millis() as u64,
})
}
}
}
}
},
)
.spawn()
.build()
}
pub fn create_anchor_detach_handler(manager: Arc<AnchorManager>) -> crate::messenger::Handler {
crate::messenger::Handler::typed_unary_async(
"_anchor_detach",
move |ctx: crate::messenger::TypedContext<AnchorDetachRequest>| {
let manager = manager.clone();
async move {
let started = Instant::now();
let req = ctx.input;
let (_, local_id) = req.handle.unpack();
use dashmap::mapref::entry::Entry;
let maybe_entry_info = match manager.registry.entry(local_id) {
Entry::Vacant(_) => None,
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
entry.attachment = false;
Some((entry.active_pump_token.take(), entry.frame_tx.clone()))
}
};
if let Some((maybe_pump_token, frame_tx)) = maybe_entry_info {
if let Some(pump_token) = maybe_pump_token {
pump_token.cancel();
}
let sentinel_bytes = crate::streaming::sender::cached_detached().clone();
let _ = frame_tx.try_send(sentinel_bytes);
manager.record_streaming_operation(
StreamingOp::Detach,
HandlerOutcome::Success,
"velo",
started,
);
} else {
manager.record_streaming_operation(
StreamingOp::Detach,
HandlerOutcome::Error,
"velo",
started,
);
}
Ok(())
}
},
)
.spawn()
.build()
}
pub fn create_anchor_finalize_handler(manager: Arc<AnchorManager>) -> crate::messenger::Handler {
crate::messenger::Handler::typed_unary_async(
"_anchor_finalize",
move |ctx: crate::messenger::TypedContext<AnchorFinalizeRequest>| {
let manager = manager.clone();
async move {
let started = Instant::now();
let req = ctx.input;
let (_, local_id) = req.handle.unpack();
if let Some(entry) = manager.remove_anchor(local_id) {
let sentinel_bytes = crate::streaming::sender::cached_finalized().clone();
let _ = entry.frame_tx.try_send(sentinel_bytes);
manager.record_streaming_operation(
StreamingOp::Finalize,
HandlerOutcome::Success,
"velo",
started,
);
} else {
manager.record_streaming_operation(
StreamingOp::Finalize,
HandlerOutcome::Error,
"velo",
started,
);
}
Ok(())
}
},
)
.spawn()
.build()
}
pub fn create_anchor_cancel_handler(manager: Arc<AnchorManager>) -> crate::messenger::Handler {
crate::messenger::Handler::typed_unary_async(
"_anchor_cancel",
move |ctx: crate::messenger::TypedContext<AnchorCancelRequest>| {
let manager = manager.clone();
async move {
let started = Instant::now();
let req = ctx.input;
let (_, local_id) = req.handle.unpack();
if let Some(entry) = manager.remove_anchor(local_id) {
entry.cancel_token.cancel();
manager.record_streaming_operation(
StreamingOp::Cancel,
HandlerOutcome::Success,
"velo",
started,
);
} else {
manager.record_streaming_operation(
StreamingOp::Cancel,
HandlerOutcome::Error,
"velo",
started,
);
}
Ok(())
}
},
)
.spawn()
.build()
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Result as AnyhowResult;
use futures::StreamExt;
use futures::future::BoxFuture;
use std::sync::Arc;
struct MockFrameTransport;
impl crate::streaming::transport::FrameTransport for MockFrameTransport {
fn bind(
&self,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<(String, flume::Receiver<Vec<u8>>)>> {
Box::pin(async {
Ok((
"mock://test-endpoint".to_string(),
flume::bounded::<Vec<u8>>(256).1,
))
})
}
fn connect(
&self,
_endpoint: &str,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<flume::Sender<Vec<u8>>>> {
Box::pin(async { Ok(flume::bounded::<Vec<u8>>(256).0) })
}
}
fn make_test_manager() -> Arc<AnchorManager> {
let worker_id = velo_ext::WorkerId::from_u64(1);
let transport = Arc::new(MockFrameTransport);
Arc::new(AnchorManager::new(worker_id, transport))
}
#[test]
fn test_anchor_attach_response_serde_ok() {
let resp = AnchorAttachResponse::Ok {
stream_endpoint: "mock://test-endpoint".to_string(),
heartbeat_interval_ms: 5000,
};
let json = serde_json::to_string(&resp).expect("serialize Ok");
let decoded: AnchorAttachResponse = serde_json::from_str(&json).expect("deserialize Ok");
match decoded {
AnchorAttachResponse::Ok {
stream_endpoint,
heartbeat_interval_ms,
} => {
assert_eq!(stream_endpoint, "mock://test-endpoint");
assert_eq!(heartbeat_interval_ms, 5000);
}
other => panic!("expected Ok, got {:?}", other),
}
}
#[test]
fn test_anchor_attach_response_rmp_round_trip_non_default_heartbeat() {
let resp = AnchorAttachResponse::Ok {
stream_endpoint: "tcp://10.0.0.1:9000".to_string(),
heartbeat_interval_ms: 1234,
};
let bytes = rmp_serde::to_vec(&resp).expect("rmp serialize Ok");
let decoded: AnchorAttachResponse =
rmp_serde::from_slice(&bytes).expect("rmp deserialize Ok");
match decoded {
AnchorAttachResponse::Ok {
stream_endpoint,
heartbeat_interval_ms,
} => {
assert_eq!(stream_endpoint, "tcp://10.0.0.1:9000");
assert_eq!(heartbeat_interval_ms, 1234);
}
other => panic!("expected Ok, got {:?}", other),
}
}
#[test]
fn test_anchor_attach_response_serde_ok_backcompat_default_heartbeat() {
let legacy_json = r#"{"Ok":{"stream_endpoint":"mock://legacy"}}"#;
let decoded: AnchorAttachResponse =
serde_json::from_str(legacy_json).expect("legacy Ok response must deserialize");
match decoded {
AnchorAttachResponse::Ok {
stream_endpoint,
heartbeat_interval_ms,
} => {
assert_eq!(stream_endpoint, "mock://legacy");
assert_eq!(
heartbeat_interval_ms, 5000,
"missing field must default to 5000ms"
);
}
other => panic!("expected Ok, got {:?}", other),
}
}
#[test]
fn test_anchor_attach_response_serde_err() {
let resp = AnchorAttachResponse::Err {
reason: "already attached".to_string(),
};
let json = serde_json::to_string(&resp).expect("serialize Err");
let decoded: AnchorAttachResponse = serde_json::from_str(&json).expect("deserialize Err");
match decoded {
AnchorAttachResponse::Err { reason } => {
assert!(reason.contains("already attached"));
}
other => panic!("expected Err, got {:?}", other),
}
}
#[tokio::test]
async fn test_anchor_attach_handler() {
let manager = make_test_manager();
let anchor = manager.create_anchor::<u8>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
let (endpoint, _receiver) = manager.transport.bind(local_id, 0).await.unwrap();
use dashmap::mapref::entry::Entry;
let result = match manager.registry.entry(local_id) {
Entry::Vacant(_) => AnchorAttachResponse::Err {
reason: format!("anchor {} not found", handle),
},
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if entry.attachment {
AnchorAttachResponse::Err {
reason: format!("anchor {} already attached", handle),
}
} else {
entry.attachment = true;
AnchorAttachResponse::Ok {
stream_endpoint: endpoint,
heartbeat_interval_ms: 5000,
}
}
}
};
match result {
AnchorAttachResponse::Ok {
stream_endpoint, ..
} => {
assert_eq!(stream_endpoint, "mock://test-endpoint");
}
other => panic!("expected Ok, got {:?}", other),
}
assert!(
manager
.registry
.get(&local_id)
.map(|e| e.attachment)
.unwrap_or(false),
"attachment must be true after attach"
);
let _handler = create_anchor_attach_handler(manager.clone());
}
#[tokio::test]
async fn test_anchor_attach_already_attached() {
let manager = make_test_manager();
let anchor = manager.create_anchor::<u8>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
{
use dashmap::mapref::entry::Entry;
if let Entry::Occupied(mut occ) = manager.registry.entry(local_id) {
let entry = occ.get_mut();
entry.attachment = true;
}
}
use dashmap::mapref::entry::Entry;
let result = match manager.registry.entry(local_id) {
Entry::Vacant(_) => AnchorAttachResponse::Err {
reason: format!("anchor {} not found", handle),
},
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if entry.attachment {
AnchorAttachResponse::Err {
reason: format!("anchor {} already attached", handle),
}
} else {
AnchorAttachResponse::Ok {
stream_endpoint: "unreachable".to_string(),
heartbeat_interval_ms: 5000,
}
}
}
};
match result {
AnchorAttachResponse::Err { reason } => {
assert!(
reason.contains("already attached"),
"reason must mention 'already attached', got: {reason}"
);
}
other => panic!("expected Err, got {:?}", other),
}
}
#[tokio::test]
async fn test_anchor_attach_not_found() {
let manager = make_test_manager();
let fake_handle = StreamAnchorHandle::pack(velo_ext::WorkerId::from_u64(1), 9999);
use dashmap::mapref::entry::Entry;
let local_id = 9999u64;
let result = match manager.registry.entry(local_id) {
Entry::Vacant(_) => AnchorAttachResponse::Err {
reason: format!("anchor {} not found", fake_handle),
},
Entry::Occupied(_) => panic!("should not be occupied"),
};
match result {
AnchorAttachResponse::Err { reason } => {
assert!(
reason.contains("not found"),
"reason must mention 'not found', got: {reason}"
);
}
other => panic!("expected Err, got {:?}", other),
}
}
#[tokio::test]
async fn test_anchor_detach_handler() {
let manager = make_test_manager();
let mut stream = manager.create_anchor::<Vec<u8>>();
let handle = stream.handle();
let (_, local_id) = handle.unpack();
{
use dashmap::mapref::entry::Entry;
if let Entry::Occupied(mut occ) = manager.registry.entry(local_id) {
let entry = occ.get_mut();
entry.attachment = true;
}
}
use dashmap::mapref::entry::Entry;
let maybe_entry_info = match manager.registry.entry(local_id) {
Entry::Vacant(_) => None,
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
entry.attachment = false;
Some((entry.active_pump_token.take(), entry.frame_tx.clone()))
}
};
if let Some((maybe_pump_token, frame_tx)) = maybe_entry_info {
if let Some(pump_token) = maybe_pump_token {
pump_token.cancel();
}
let sentinel_bytes =
rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::<Vec<u8>>::Detached)
.expect("serialize Detached sentinel");
let _ = frame_tx.try_send(sentinel_bytes);
}
assert!(
manager
.registry
.get(&local_id)
.map(|e| !e.attachment)
.unwrap_or(false),
"attachment must be false after detach"
);
assert!(
manager.registry.contains_key(&local_id),
"anchor must remain in registry after detach"
);
let result = stream.next().await;
assert!(
matches!(
result,
Some(Ok(crate::streaming::frame::StreamFrame::Detached))
),
"sentinel must be Detached, got {:?}",
result
);
let _handler = create_anchor_detach_handler(manager.clone());
}
#[tokio::test]
async fn test_anchor_finalize_handler() {
let manager = make_test_manager();
let mut stream = manager.create_anchor::<Vec<u8>>();
let handle = stream.handle();
let (_, local_id) = handle.unpack();
{
use dashmap::mapref::entry::Entry;
if let Entry::Occupied(mut occ) = manager.registry.entry(local_id) {
let entry = occ.get_mut();
entry.attachment = true;
}
}
if let Some(entry) = manager.remove_anchor(local_id) {
let sentinel_bytes =
rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::<Vec<u8>>::Finalized)
.expect("serialize Finalized sentinel");
let _ = entry.frame_tx.try_send(sentinel_bytes);
}
assert!(
!manager.registry.contains_key(&local_id),
"anchor must be absent from registry after finalize"
);
let result = stream.next().await;
assert!(
matches!(
result,
Some(Ok(crate::streaming::frame::StreamFrame::Finalized))
),
"sentinel must be Finalized, got {:?}",
result
);
let _handler = create_anchor_finalize_handler(manager.clone());
}
#[tokio::test]
async fn test_anchor_cancel_handler() {
let manager = make_test_manager();
let anchor = manager.create_anchor::<u8>();
let (_, local_id) = anchor.handle().unpack();
if let Some(entry) = manager.remove_anchor(local_id) {
entry.cancel_token.cancel();
}
assert!(
!manager.registry.contains_key(&local_id),
"anchor must be absent after cancel"
);
if let Some(entry) = manager.remove_anchor(local_id) {
entry.cancel_token.cancel();
}
let _handler = create_anchor_cancel_handler(manager.clone());
}
#[allow(clippy::type_complexity)]
fn make_pump_test_infra() -> (
flume::Sender<Vec<u8>>, // transport_tx: simulates transport frames
flume::Receiver<Vec<u8>>, // frame_rx: where pump writes to (consumer side)
tokio_util::sync::CancellationToken,
std::sync::Arc<dashmap::DashMap<u64, crate::streaming::anchor::AnchorEntry>>,
u64, // local_id
) {
let (transport_tx, transport_rx) = flume::bounded::<Vec<u8>>(256);
let (frame_tx, frame_rx) = flume::bounded::<Vec<u8>>(256);
let cancel_token = tokio_util::sync::CancellationToken::new();
let registry = std::sync::Arc::new(dashmap::DashMap::new());
let local_id = 1u64;
registry.insert(
local_id,
crate::streaming::anchor::AnchorEntry {
frame_tx: frame_tx.clone(),
cancel_token: cancel_token.clone(),
active_pump_token: None,
attachment: true,
timeout_cancel: None,
unattached_timeout: None,
heartbeat_interval: Duration::from_secs(5),
stream_cancel_handle: None,
},
);
let pump_cancel = cancel_token.clone();
let ctx = crate::streaming::anchor::AnchorContext {
registry: registry.clone(),
mpsc_registry: std::sync::Arc::new(dashmap::DashMap::new()),
metrics: None,
};
tokio::spawn(reader_pump(
transport_rx,
frame_tx,
pump_cancel,
ctx,
local_id,
Duration::from_secs(5),
));
(transport_tx, frame_rx, cancel_token, registry, local_id)
}
#[tokio::test]
async fn test_pump_forwards_data_frames() {
let (transport_tx, frame_rx, _cancel, _registry, _id) = make_pump_test_infra();
let data_bytes =
rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::Item(42u32)).unwrap();
transport_tx.send_async(data_bytes.clone()).await.unwrap();
let received =
tokio::time::timeout(std::time::Duration::from_millis(500), frame_rx.recv_async())
.await
.expect("timeout waiting for frame")
.expect("frame_rx closed");
assert_eq!(received, data_bytes, "pump must forward bytes unchanged");
}
#[tokio::test]
async fn test_pump_resets_heartbeat_counter_on_frame() {
tokio::time::pause();
let (transport_tx, frame_rx, _cancel, registry, local_id) = make_pump_test_infra();
tokio::time::sleep(std::time::Duration::from_millis(4500)).await;
let hb_bytes =
rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::<()>::Heartbeat).unwrap();
transport_tx.send_async(hb_bytes).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(4500)).await;
transport_tx
.send_async(
rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::<()>::Heartbeat).unwrap(),
)
.await
.unwrap();
while frame_rx.try_recv().is_ok() {}
assert!(
registry.contains_key(&local_id),
"anchor must still be in registry -- heartbeat counter was reset"
);
}
#[tokio::test]
async fn test_pump_injects_dropped_after_3_missed_heartbeats() {
tokio::time::pause();
let (transport_tx, frame_rx, _cancel, _registry, _id) = make_pump_test_infra();
tokio::time::sleep(std::time::Duration::from_secs(16)).await;
let mut frames = Vec::new();
while let Ok(bytes) = frame_rx.try_recv() {
frames.push(bytes);
}
assert!(
!frames.is_empty(),
"must have received at least one frame (Dropped sentinel)"
);
let last = frames.last().unwrap();
let decoded: crate::streaming::frame::StreamFrame<()> =
rmp_serde::from_slice(last).expect("deserialize");
assert!(
matches!(decoded, crate::streaming::frame::StreamFrame::Dropped),
"last frame must be Dropped, got {:?}",
decoded
);
drop(transport_tx);
}
#[tokio::test]
async fn test_pump_removes_registry_entry_after_3_missed_heartbeats() {
tokio::time::pause();
let (transport_tx, _frame_rx, _cancel, registry, local_id) = make_pump_test_infra();
tokio::time::sleep(std::time::Duration::from_secs(16)).await;
assert!(
!registry.contains_key(&local_id),
"anchor must be removed from registry after 3 missed heartbeats (LIVE-02)"
);
drop(transport_tx);
}
#[tokio::test]
async fn test_pump_exits_when_cancel_token_cancelled() {
let (transport_tx, frame_rx, cancel_token, registry, local_id) = make_pump_test_infra();
cancel_token.cancel();
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let data = rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::Item(99u32)).unwrap();
let _ = transport_tx.try_send(data);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
frame_rx.try_recv().is_err(),
"no frames should be forwarded after cancel"
);
assert!(cancel_token.is_cancelled());
let _ = (registry, local_id);
}
#[tokio::test]
async fn test_pump_exits_when_transport_closes() {
let (transport_tx, _frame_rx, cancel_token, _registry, _id) = make_pump_test_infra();
drop(transport_tx);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
cancel_token.is_cancelled(),
"cancel_token must be cancelled after pump exits due to transport close"
);
}
#[tokio::test]
async fn test_child_token_reattach_pump_survives() {
let parent = tokio_util::sync::CancellationToken::new();
let (frame_tx, frame_rx) = flume::bounded::<Vec<u8>>(256);
let registry = std::sync::Arc::new(dashmap::DashMap::new());
let local_id = 1u64;
let (tx1, rx1) = flume::bounded::<Vec<u8>>(256);
let child1 = parent.child_token();
registry.insert(
local_id,
crate::streaming::anchor::AnchorEntry {
frame_tx: frame_tx.clone(),
cancel_token: parent.clone(),
active_pump_token: Some(child1.clone()),
attachment: true,
timeout_cancel: None,
unattached_timeout: None,
heartbeat_interval: Duration::from_secs(5),
stream_cancel_handle: None,
},
);
let mpsc_reg: std::sync::Arc<
dashmap::DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>,
> = std::sync::Arc::new(dashmap::DashMap::new());
let ctx1 = crate::streaming::anchor::AnchorContext {
registry: registry.clone(),
mpsc_registry: mpsc_reg.clone(),
metrics: None,
};
tokio::spawn(reader_pump(
rx1,
frame_tx.clone(),
child1.clone(),
ctx1,
local_id,
Duration::from_secs(5),
));
let data1 = rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::Item(1u32)).unwrap();
tx1.send_async(data1.clone()).await.unwrap();
let received =
tokio::time::timeout(std::time::Duration::from_millis(500), frame_rx.recv_async())
.await
.expect("timeout")
.expect("closed");
assert_eq!(received, data1, "first pump must forward data");
child1.cancel();
assert!(
!parent.is_cancelled(),
"parent must NOT be cancelled by child cancel"
);
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
let (tx2, rx2) = flume::bounded::<Vec<u8>>(256);
let child2 = parent.child_token();
if let Some(mut entry) = registry.get_mut(&local_id) {
entry.active_pump_token = Some(child2.clone());
entry.attachment = true;
}
let ctx2 = crate::streaming::anchor::AnchorContext {
registry: registry.clone(),
mpsc_registry: mpsc_reg.clone(),
metrics: None,
};
tokio::spawn(reader_pump(
rx2,
frame_tx.clone(),
child2.clone(),
ctx2,
local_id,
Duration::from_secs(5),
));
let data2 = rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::Item(2u32)).unwrap();
tx2.send_async(data2.clone()).await.unwrap();
let received2 =
tokio::time::timeout(std::time::Duration::from_millis(500), frame_rx.recv_async())
.await
.expect("timeout on reattach")
.expect("closed on reattach");
assert_eq!(
received2, data2,
"second pump must forward data after reattach"
);
parent.cancel();
assert!(
child2.is_cancelled(),
"child must be cancelled when parent is cancelled"
);
}
#[test]
fn test_stream_cancel_handle_pack_unpack() {
let worker_id = velo_ext::WorkerId::from_u64(0xDEAD_BEEF_1234_5678);
let stream_id: u64 = 0xABCD_EF01_2345_6789;
let handle = crate::streaming::control::StreamCancelHandle::pack(worker_id, stream_id);
let (recovered_worker, recovered_stream) = handle.unpack();
assert_eq!(
recovered_worker, worker_id,
"worker_id must round-trip through pack/unpack"
);
assert_eq!(
recovered_stream, stream_id,
"stream_id must round-trip through pack/unpack"
);
}
#[test]
fn test_stream_cancel_handle_serde() {
let worker_id = velo_ext::WorkerId::from_u64(0xCAFE_BABE_0000_0001);
let stream_id: u64 = 42;
let handle = crate::streaming::control::StreamCancelHandle::pack(worker_id, stream_id);
let encoded = rmp_serde::to_vec(&handle).expect("rmp_serde serialize must succeed");
let decoded: crate::streaming::control::StreamCancelHandle =
rmp_serde::from_slice(&encoded).expect("rmp_serde deserialize must succeed");
assert_eq!(
handle, decoded,
"StreamCancelHandle must survive rmp_serde round-trip"
);
let (w, s) = decoded.unpack();
assert_eq!(w, worker_id);
assert_eq!(s, stream_id);
}
#[test]
fn test_stream_cancel_handler_compiles() {
let registry = std::sync::Arc::new(crate::streaming::control::SenderRegistry::default());
let _handler = crate::streaming::control::create_stream_cancel_handler(registry);
}
#[tokio::test]
async fn test_pump_exits_when_consumer_drops() {
let (transport_tx, frame_rx, cancel_token, _registry, _id) = make_pump_test_infra();
drop(frame_rx);
let data = rmp_serde::to_vec(&crate::streaming::frame::StreamFrame::Item(1u32)).unwrap();
let _ = transport_tx.send_async(data).await;
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
assert!(
cancel_token.is_cancelled(),
"cancel_token must be cancelled after pump exits due to consumer drop"
);
}
}