use std::collections::HashMap;
use std::pin::Pin;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicU64, Ordering},
};
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use crate::observability::{HandlerOutcome, StreamingOp, VeloMetrics};
use dashmap::DashMap;
use derive_builder::Builder;
use futures::Stream;
use serde::de::DeserializeOwned;
use tokio_util::sync::CancellationToken;
use crate::streaming::frame::{StreamError, StreamFrame};
use crate::streaming::handle::StreamAnchorHandle;
pub(crate) fn set_active_anchor_gauge(
metrics: Option<&Arc<VeloMetrics>>,
spsc: &Arc<DashMap<u64, AnchorEntry>>,
mpsc: &Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
) {
if let Some(m) = metrics {
m.set_streaming_active_anchors(spsc.len() + mpsc.len());
}
}
#[derive(Clone)]
pub(crate) struct AnchorContext {
pub registry: Arc<DashMap<u64, AnchorEntry>>,
pub mpsc_registry: Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
pub metrics: Option<Arc<VeloMetrics>>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AttachError {
#[error("anchor {handle} not found in registry")]
AnchorNotFound { handle: StreamAnchorHandle },
#[error("anchor {handle} is already attached")]
AlreadyAttached { handle: StreamAnchorHandle },
#[error("anchor {handle} reached max_senders limit of {limit}")]
MaxSendersReached {
handle: StreamAnchorHandle,
limit: usize,
},
#[error("anchor {handle} is of wrong kind: expected {expected}")]
WrongHandleKind {
handle: StreamAnchorHandle,
expected: crate::streaming::handle::AnchorKind,
},
#[error("transport bind failed: {0}")]
TransportError(#[from] anyhow::Error),
}
#[derive(Debug, Clone, Default)]
pub struct AnchorConfig {
pub unattached_timeout: Option<Duration>,
pub heartbeat_interval: Option<Duration>,
}
#[allow(dead_code)]
pub(crate) struct AnchorEntry {
pub frame_tx: flume::Sender<Vec<u8>>,
pub cancel_token: CancellationToken,
pub active_pump_token: Option<CancellationToken>,
pub attachment: bool,
pub timeout_cancel: Option<CancellationToken>,
pub unattached_timeout: Option<Duration>,
pub heartbeat_interval: Duration,
pub stream_cancel_handle: Option<crate::streaming::control::StreamCancelHandle>,
}
struct StreamControllerInner {
local_id: u64,
registry: Arc<DashMap<u64, AnchorEntry>>,
mpsc_registry: Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
metrics: Option<Arc<VeloMetrics>>,
sender_registry: Arc<crate::streaming::control::SenderRegistry>,
messenger: Option<Arc<crate::messenger::Messenger>>,
cancelled: AtomicBool,
}
#[derive(Clone)]
pub struct StreamController {
inner: Arc<StreamControllerInner>,
}
impl StreamController {
pub fn cancel(&self) {
if self
.inner
.cancelled
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return; }
let started = Instant::now();
let stream_cancel_handle =
self.inner
.registry
.remove(&self.inner.local_id)
.and_then(|(_, entry)| {
entry.cancel_token.cancel();
entry.stream_cancel_handle
});
set_active_anchor_gauge(
self.inner.metrics.as_ref(),
&self.inner.registry,
&self.inner.mpsc_registry,
);
if let Some(metrics) = self.inner.metrics.as_ref() {
metrics.record_streaming_operation(
StreamingOp::Cancel,
HandlerOutcome::Success,
"velo",
started.elapsed(),
);
}
if let Some(handle) = stream_cancel_handle {
let (sender_worker_id, sender_stream_id) = handle.unpack();
if let Some((_, entry)) = self.inner.sender_registry.senders.remove(&sender_stream_id) {
drop(entry.rx_closer.lock().unwrap().take());
entry.cancel_token.cancel();
}
if let Some(messenger) = self.inner.messenger.clone() {
let payload = serde_json::to_vec(&crate::streaming::control::StreamCancelRequest {
sender_stream_id,
})
.expect("serialize StreamCancelRequest");
if let Ok(rt) = tokio::runtime::Handle::try_current() {
rt.spawn(async move {
let _ = messenger
.am_send_streaming("_stream_cancel")
.expect("am_send_streaming builder")
.raw_payload(bytes::Bytes::from(payload))
.worker(sender_worker_id)
.send()
.await;
});
}
}
}
}
}
pub struct StreamAnchor<T> {
handle: StreamAnchorHandle,
inner_stream: flume::r#async::RecvStream<'static, Vec<u8>>,
terminated: bool,
local_id: u64,
registry: Arc<DashMap<u64, AnchorEntry>>,
mpsc_registry: Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
controller: StreamController,
metrics: Option<Arc<VeloMetrics>>,
_phantom: std::marker::PhantomData<T>,
}
impl<T> StreamAnchor<T> {
pub(crate) fn new(
handle: StreamAnchorHandle,
rx: flume::Receiver<Vec<u8>>,
local_id: u64,
ctx: AnchorContext,
sender_registry: Arc<crate::streaming::control::SenderRegistry>,
messenger: Option<Arc<crate::messenger::Messenger>>,
) -> Self {
let AnchorContext {
registry,
mpsc_registry,
metrics,
} = ctx;
let inner = Arc::new(StreamControllerInner {
local_id,
registry: registry.clone(),
mpsc_registry: mpsc_registry.clone(),
metrics: metrics.clone(),
sender_registry,
messenger,
cancelled: AtomicBool::new(false),
});
let controller = StreamController { inner };
Self {
handle,
inner_stream: rx.into_stream(),
terminated: false,
local_id,
registry,
mpsc_registry,
controller,
metrics,
_phantom: std::marker::PhantomData,
}
}
pub fn handle(&self) -> StreamAnchorHandle {
self.handle
}
pub fn controller(&self) -> StreamController {
self.controller.clone()
}
pub fn cancel(mut self) -> StreamController {
self.terminated = true; self.controller.cancel();
self.controller.clone()
}
pub fn set_timeout(&self, timeout: Option<Duration>) {
if let Some(mut entry) = self.registry.get_mut(&self.local_id) {
if let Some(ref old_tc) = entry.timeout_cancel {
old_tc.cancel();
}
entry.unattached_timeout = timeout;
if !entry.attachment {
if let Some(duration) = timeout {
let tc = AnchorManager::spawn_timeout_task(
self.registry.clone(),
self.mpsc_registry.clone(),
self.metrics.clone(),
self.local_id,
duration,
&entry.cancel_token,
);
entry.timeout_cancel = Some(tc);
} else {
entry.timeout_cancel = None;
}
} else {
entry.timeout_cancel = None;
}
}
}
}
impl<T> Unpin for StreamAnchor<T> {}
impl<T> Drop for StreamAnchor<T> {
fn drop(&mut self) {
if !self.terminated {
self.controller.cancel();
}
}
}
impl<T: DeserializeOwned> Stream for StreamAnchor<T> {
type Item = Result<StreamFrame<T>, StreamError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if this.terminated {
return Poll::Ready(None);
}
loop {
match Pin::new(&mut this.inner_stream).poll_next(cx) {
Poll::Ready(Some(bytes)) => {
match rmp_serde::from_slice::<StreamFrame<T>>(&bytes) {
Ok(StreamFrame::Heartbeat) => continue, Ok(StreamFrame::Item(data)) => {
return Poll::Ready(Some(Ok(StreamFrame::Item(data))));
}
Ok(StreamFrame::SenderError(msg)) => {
return Poll::Ready(Some(Err(StreamError::SenderError(msg))));
}
Ok(StreamFrame::Finalized) => {
this.terminated = true;
if let Some((_, entry)) = this.registry.remove(&this.local_id) {
entry.cancel_token.cancel();
set_active_anchor_gauge(
this.metrics.as_ref(),
&this.registry,
&this.mpsc_registry,
);
}
return Poll::Ready(Some(Ok(StreamFrame::Finalized)));
}
Ok(StreamFrame::Detached) => {
if let Some(mut entry) = this.registry.get_mut(&this.local_id) {
entry.attachment = false;
}
return Poll::Ready(Some(Ok(StreamFrame::Detached)));
}
Ok(StreamFrame::Dropped) => {
this.terminated = true;
if let Some((_, entry)) = this.registry.remove(&this.local_id) {
entry.cancel_token.cancel();
set_active_anchor_gauge(
this.metrics.as_ref(),
&this.registry,
&this.mpsc_registry,
);
}
return Poll::Ready(Some(Err(StreamError::SenderDropped)));
}
Ok(StreamFrame::TransportError(msg)) => {
this.terminated = true;
return Poll::Ready(Some(Err(StreamError::TransportError(msg))));
}
Err(e) => {
this.terminated = true;
return Poll::Ready(Some(Err(StreamError::DeserializationError(
e.to_string(),
))));
}
}
}
Poll::Ready(None) => {
this.terminated = true;
return Poll::Ready(None);
}
Poll::Pending => return Poll::Pending,
}
}
}
}
#[derive(Builder)]
#[builder(pattern = "owned", build_fn(name = "build_inner", private))]
pub struct AnchorManager {
worker_id: velo_ext::WorkerId,
#[builder(setter(skip), default = "AtomicU64::new(0)")]
next_local_id: AtomicU64,
#[builder(default = "Arc::new(DashMap::new())")]
pub(crate) registry: Arc<DashMap<u64, AnchorEntry>>,
#[builder(default = "Arc::new(DashMap::new())")]
pub(crate) mpsc_registry: Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
pub transport: Arc<dyn crate::streaming::transport::FrameTransport>,
#[builder(default = "Arc::new(HashMap::new())")]
pub transport_registry:
Arc<HashMap<String, Arc<dyn crate::streaming::transport::FrameTransport>>>,
#[builder(default, setter(into, strip_option))]
pub default_unattached_timeout: Option<Duration>,
#[builder(default = "Duration::from_secs(5)")]
pub default_heartbeat_interval: Duration,
#[builder(default)]
pub messenger: Option<Arc<crate::messenger::Messenger>>,
#[builder(default)]
pub metrics: Option<Arc<VeloMetrics>>,
#[builder(setter(skip), default = "AtomicU64::new(0)")]
next_sender_stream_id: AtomicU64,
#[builder(setter(skip), default = "AtomicU64::new(0)")]
pub(crate) next_routing_session_id: AtomicU64,
#[builder(default = "Arc::new(crate::streaming::control::SenderRegistry::default())")]
pub sender_registry: Arc<crate::streaming::control::SenderRegistry>,
#[builder(setter(skip), default = "std::sync::OnceLock::new()")]
pub(crate) messenger_lock: std::sync::OnceLock<Arc<crate::messenger::Messenger>>,
#[builder(setter(skip), default = "std::sync::OnceLock::new()")]
mux: std::sync::OnceLock<Arc<crate::streaming::messenger_mux::MessengerMuxTransport>>,
}
impl AnchorManagerBuilder {
pub fn build(self) -> Result<AnchorManager, AnchorManagerBuilderError> {
self.build_inner()
}
}
impl AnchorManager {
pub fn new(
worker_id: velo_ext::WorkerId,
transport: Arc<dyn crate::streaming::transport::FrameTransport>,
) -> Self {
AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.build()
.expect("required fields provided")
}
pub fn create_anchor<T>(&self) -> StreamAnchor<T> {
self.create_anchor_with_config(AnchorConfig::default())
}
pub fn create_anchor_with_config<T>(&self, config: AnchorConfig) -> StreamAnchor<T> {
let local_id = self.next_local_id.fetch_add(1, Ordering::Relaxed) + 1;
let (frame_tx, frame_rx) = flume::bounded::<Vec<u8>>(256);
let cancel_token = CancellationToken::new();
let unattached_timeout = config
.unattached_timeout
.or(self.default_unattached_timeout);
let heartbeat_interval = config
.heartbeat_interval
.unwrap_or(self.default_heartbeat_interval);
let timeout_cancel = unattached_timeout.map(|timeout| {
Self::spawn_timeout_task(
self.registry.clone(),
self.mpsc_registry.clone(),
self.metrics.clone(),
local_id,
timeout,
&cancel_token,
)
});
let entry = AnchorEntry {
frame_tx,
cancel_token,
active_pump_token: None,
attachment: false,
timeout_cancel,
unattached_timeout,
heartbeat_interval,
stream_cancel_handle: None, };
self.registry.insert(local_id, entry);
self.update_active_anchor_gauge();
let handle = StreamAnchorHandle::pack(self.worker_id, local_id);
StreamAnchor::new(
handle,
frame_rx,
local_id,
self.anchor_context(),
self.sender_registry.clone(),
self.messenger.clone(),
)
}
pub(crate) fn spawn_timeout_task(
registry: Arc<DashMap<u64, AnchorEntry>>,
mpsc_registry: Arc<DashMap<u64, crate::streaming::mpsc::anchor::MpscAnchorEntry>>,
metrics: Option<Arc<VeloMetrics>>,
local_id: u64,
timeout: Duration,
parent_cancel: &CancellationToken,
) -> CancellationToken {
let tc = parent_cancel.child_token();
let tc_clone = tc.clone();
tokio::spawn(async move {
tokio::select! {
_ = tc_clone.cancelled() => {
}
_ = tokio::time::sleep(timeout) => {
if let Some((_, entry)) = registry.remove(&local_id) {
entry.cancel_token.cancel();
set_active_anchor_gauge(metrics.as_ref(), ®istry, &mpsc_registry);
}
}
}
});
tc
}
#[allow(dead_code)]
pub(crate) fn remove_anchor(&self, local_id: u64) -> Option<AnchorEntry> {
self.registry.remove(&local_id).map(|(_, entry)| {
entry.cancel_token.cancel();
self.update_active_anchor_gauge();
entry
})
}
#[allow(dead_code)]
pub(crate) fn inject_sentinel(&self, local_id: u64, frame_bytes: Vec<u8>) {
let maybe_sender = self
.registry
.get(&local_id)
.map(|entry| entry.frame_tx.clone());
if let Some(sender) = maybe_sender {
let _ = sender.try_send(frame_bytes);
}
}
pub(crate) fn install_mux(
&self,
mux: Arc<crate::streaming::messenger_mux::MessengerMuxTransport>,
) -> anyhow::Result<()> {
self.mux
.set(mux)
.map_err(|_| anyhow::anyhow!("a messenger mux is already installed on this manager"))
}
pub(crate) fn flush_mux_batches(&self) {
if let Some(mux) = self.mux.get() {
mux.flush_batches();
}
}
fn supported_transport_keys(&self) -> Vec<velo_ext::TransportKey> {
crate::streaming::negotiation::advertised_keys(
&self.transport_registry,
&self.transport,
self.mux.get(),
)
}
pub(crate) fn select_streaming_transport(
&self,
offered: &[velo_ext::TransportKey],
) -> crate::streaming::negotiation::Selection {
crate::streaming::negotiation::select(offered, self.mux.get(), &self.transport)
}
async fn connect_streaming(
&self,
key: &velo_ext::TransportKey,
peer: velo_ext::WorkerId,
anchor_id: u64,
session_id: u64,
initial_credit: u32,
slot_byte_budget: u32,
) -> Result<flume::Sender<Vec<u8>>, AttachError> {
match crate::streaming::negotiation::choose(key, initial_credit, slot_byte_budget) {
Ok(crate::streaming::negotiation::Connect::Mux(limits)) => {
let mux = self.mux.get().ok_or_else(|| {
AttachError::TransportError(anyhow::anyhow!(
"peer answered with {key} but no messenger mux is installed here; \
it can only have learned that key from an advertisement this node made"
))
})?;
Ok(mux
.connect_negotiated(peer, anchor_id, session_id, limits)
.await?)
}
Ok(crate::streaming::negotiation::Connect::Legacy) => {
let transport = self.resolve_transport(key)?;
Ok(transport.connect(peer, anchor_id, session_id).await?)
}
Err(error) => Err(AttachError::TransportError(anyhow::anyhow!(
"peer answered with {key} but {error}"
))),
}
}
fn resolve_transport(
&self,
key: &velo_ext::TransportKey,
) -> Result<Arc<dyn crate::streaming::transport::FrameTransport>, AttachError> {
if let Some(transport) = self.transport_registry.get(key.as_str()) {
return Ok(Arc::clone(transport));
}
if self.transport_registry.is_empty() {
return Ok(Arc::clone(&self.transport));
}
Err(AttachError::TransportError(anyhow::anyhow!(
"unsupported streaming transport key: {}",
key
)))
}
#[allow(dead_code)]
pub(crate) fn try_attach(
&self,
local_id: u64,
handle: StreamAnchorHandle,
) -> Result<(), AttachError> {
use dashmap::mapref::entry::Entry;
match self.registry.entry(local_id) {
Entry::Vacant(_) => Err(AttachError::AnchorNotFound { handle }),
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if entry.attachment {
Err(AttachError::AlreadyAttached { handle })
} else {
entry.attachment = true;
if let Some(ref tc) = entry.timeout_cancel {
tc.cancel();
}
Ok(())
}
}
}
}
#[allow(dead_code)]
pub(crate) fn detach(&self, local_id: u64) -> bool {
let (was_attached, maybe_timeout, maybe_parent) = self
.registry
.get_mut(&local_id)
.map(|mut entry| {
let was = entry.attachment;
entry.attachment = false;
(
was,
entry.unattached_timeout,
Some(entry.cancel_token.clone()),
)
})
.unwrap_or((false, None, None));
if let Some(timeout) = maybe_timeout {
let parent = maybe_parent
.as_ref()
.expect("cancel_token present when unattached_timeout is");
let tc = Self::spawn_timeout_task(
self.registry.clone(),
self.mpsc_registry.clone(),
self.metrics.clone(),
local_id,
timeout,
parent,
);
if let Some(mut entry) = self.registry.get_mut(&local_id) {
entry.timeout_cancel = Some(tc);
}
}
was_attached
}
pub fn active_anchor_count(&self) -> usize {
self.registry.len()
}
pub(crate) fn update_active_anchor_gauge(&self) {
set_active_anchor_gauge(self.metrics.as_ref(), &self.registry, &self.mpsc_registry);
}
pub(crate) fn anchor_context(&self) -> AnchorContext {
AnchorContext {
registry: self.registry.clone(),
mpsc_registry: self.mpsc_registry.clone(),
metrics: self.metrics.clone(),
}
}
pub(crate) fn record_streaming_operation(
&self,
operation: StreamingOp,
outcome: HandlerOutcome,
transport_scheme: &str,
started: Instant,
) {
if let Some(metrics) = self.metrics.as_ref() {
metrics.record_streaming_operation(
operation,
outcome,
transport_scheme,
started.elapsed(),
);
}
}
pub fn register_handlers(
self: &Arc<Self>,
messenger: Arc<crate::messenger::Messenger>,
) -> anyhow::Result<()> {
use crate::streaming::control::{
create_anchor_attach_handler, create_anchor_cancel_handler,
create_anchor_detach_handler, create_anchor_finalize_handler,
create_stream_cancel_handler,
};
messenger.register_streaming_handler(create_anchor_attach_handler(Arc::clone(self)))?;
messenger.register_streaming_handler(create_anchor_detach_handler(Arc::clone(self)))?;
messenger.register_streaming_handler(create_anchor_finalize_handler(Arc::clone(self)))?;
messenger.register_streaming_handler(create_anchor_cancel_handler(Arc::clone(self)))?;
messenger.register_streaming_handler(create_stream_cancel_handler(Arc::clone(
&self.sender_registry,
)))?;
messenger.register_streaming_handler(
crate::streaming::mpsc::control::create_mpsc_anchor_attach_handler(Arc::clone(self)),
)?;
messenger.register_streaming_handler(
crate::streaming::mpsc::control::create_mpsc_anchor_detach_handler(Arc::clone(self)),
)?;
messenger.register_streaming_handler(
crate::streaming::mpsc::control::create_mpsc_anchor_cancel_handler(Arc::clone(self)),
)?;
self.messenger_lock
.set(messenger)
.map_err(|_| anyhow::anyhow!("register_handlers called twice"))?;
Ok(())
}
async fn attach_remote<T: serde::Serialize>(
&self,
handle: StreamAnchorHandle,
) -> Result<crate::streaming::sender::StreamSender<T>, AttachError> {
let (handle_worker_id, _) = handle.unpack();
let messenger = self.messenger_lock.get().ok_or_else(|| {
AttachError::TransportError(anyhow::anyhow!(
"register_handlers not called — messenger unavailable for remote attach"
))
})?;
let sender_stream_id = self.next_sender_stream_id.fetch_add(1, Ordering::Relaxed) + 1;
let cancel_token = tokio_util::sync::CancellationToken::new();
let (poison_tx, poison_rx) = flume::bounded::<()>(1);
let stream_cancel_handle =
crate::streaming::control::StreamCancelHandle::pack(self.worker_id, sender_stream_id);
let req = crate::streaming::control::AnchorAttachRequest {
handle,
session_id: sender_stream_id,
stream_cancel_handle,
supported_transport_keys: self.supported_transport_keys(),
};
let response: crate::streaming::control::AnchorAttachResponse = messenger
.typed_unary_streaming::<crate::streaming::control::AnchorAttachResponse>(
"_anchor_attach",
)
.payload(&req)
.map_err(AttachError::TransportError)?
.worker(handle_worker_id)
.send()
.await
.map_err(AttachError::TransportError)?;
match response {
crate::streaming::control::AnchorAttachResponse::Ok {
streaming_transport_key,
heartbeat_interval_ms,
routing_session_id,
initial_credit,
slot_byte_budget,
} => {
let (_, local_id) = handle.unpack();
let connect_session_id = if routing_session_id != 0 {
routing_session_id
} else {
sender_stream_id
};
let frame_tx = self
.connect_streaming(
&streaming_transport_key,
handle_worker_id,
local_id,
connect_session_id,
initial_credit,
slot_byte_budget,
)
.await?;
let sender_entry = crate::streaming::control::SenderEntry {
cancel_token: cancel_token.clone(),
rx_closer: std::sync::Mutex::new(Some(poison_rx)),
};
self.sender_registry
.senders
.insert(sender_stream_id, sender_entry);
Ok(crate::streaming::sender::StreamSender::new(
frame_tx,
handle,
self.registry.clone(), crate::streaming::sender::StreamSenderCancelInfo {
cancel_token,
sender_stream_id,
sender_registry: self.sender_registry.clone(),
poison_tx,
},
Duration::from_millis(heartbeat_interval_ms),
self.metrics.clone(),
Some(streaming_transport_key),
))
}
crate::streaming::control::AnchorAttachResponse::Err { reason } => {
Err(AttachError::TransportError(anyhow::anyhow!("{}", reason)))
}
}
}
pub async fn attach_stream_anchor<T: serde::Serialize>(
&self,
handle: StreamAnchorHandle,
) -> Result<crate::streaming::sender::StreamSender<T>, AttachError> {
if handle.is_mpsc_stream() {
return Err(AttachError::WrongHandleKind {
handle,
expected: crate::streaming::handle::AnchorKind::Spsc,
});
}
let (handle_worker_id, local_id) = handle.unpack();
if handle_worker_id != self.worker_id {
return self.attach_remote::<T>(handle).await;
}
{
let entry = self.registry.get(&local_id);
match entry {
None => return Err(AttachError::AnchorNotFound { handle }),
Some(e) if e.attachment => {
return Err(AttachError::AlreadyAttached { handle });
}
_ => {} }
}
use dashmap::mapref::entry::Entry;
match self.registry.entry(local_id) {
Entry::Vacant(_) => Err(AttachError::AnchorNotFound { handle }),
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if entry.attachment {
Err(AttachError::AlreadyAttached { handle })
} else {
let frame_tx = entry.frame_tx.clone();
let heartbeat_interval = entry.heartbeat_interval;
entry.attachment = true;
if let Some(ref tc) = entry.timeout_cancel {
tc.cancel();
}
let sender_stream_id =
self.next_sender_stream_id.fetch_add(1, Ordering::Relaxed) + 1;
let cancel_token = tokio_util::sync::CancellationToken::new();
let (poison_tx, poison_rx) = flume::bounded::<()>(1);
let sender_entry = crate::streaming::control::SenderEntry {
cancel_token: cancel_token.clone(),
rx_closer: std::sync::Mutex::new(Some(poison_rx)),
};
self.sender_registry
.senders
.insert(sender_stream_id, sender_entry);
entry.stream_cancel_handle =
Some(crate::streaming::control::StreamCancelHandle::pack(
self.worker_id,
sender_stream_id,
));
Ok(crate::streaming::sender::StreamSender::new(
frame_tx,
handle,
self.registry.clone(),
crate::streaming::sender::StreamSenderCancelInfo {
cancel_token,
sender_stream_id,
sender_registry: self.sender_registry.clone(),
poison_tx,
},
heartbeat_interval,
self.metrics.clone(),
None,
))
}
}
}
}
pub fn create_mpsc_anchor<T>(&self) -> crate::streaming::mpsc::MpscStreamAnchor<T> {
self.create_mpsc_anchor_with_config(crate::streaming::mpsc::MpscAnchorConfig::default())
}
pub fn create_mpsc_anchor_with_config<T>(
&self,
config: crate::streaming::mpsc::MpscAnchorConfig,
) -> crate::streaming::mpsc::MpscStreamAnchor<T> {
let raw_local = self.next_local_id.fetch_add(1, Ordering::Relaxed) + 1;
let handle = StreamAnchorHandle::pack_mpsc(self.worker_id, raw_local);
let (_, local_id) = handle.unpack();
let capacity = config.channel_capacity.unwrap_or(256);
let (frame_tx, frame_rx) = flume::bounded::<(u64, Vec<u8>)>(capacity);
let cancel_token = CancellationToken::new();
let unattached_timeout = config
.unattached_timeout
.or(self.default_unattached_timeout);
let heartbeat_interval = config
.heartbeat_interval
.unwrap_or(self.default_heartbeat_interval);
let timeout_cancel = unattached_timeout.map(|timeout| {
crate::streaming::mpsc::anchor::spawn_mpsc_timeout_task_with_metrics(
self.mpsc_registry.clone(),
Some(self.registry.clone()),
self.metrics.clone(),
local_id,
timeout,
&cancel_token,
)
});
let entry = crate::streaming::mpsc::anchor::MpscAnchorEntry {
frame_tx,
cancel_token,
senders: HashMap::new(),
next_sender_id: 1,
unattached_timeout,
timeout_cancel,
heartbeat_interval,
max_senders: config.max_senders,
spsc_registry: self.registry.clone(),
metrics: self.metrics.clone(),
};
self.mpsc_registry.insert(local_id, entry);
self.update_active_anchor_gauge();
crate::streaming::mpsc::MpscStreamAnchor::new(
handle,
frame_rx,
local_id,
self.anchor_context(),
self.sender_registry.clone(),
self.messenger.clone(),
)
}
pub async fn attach_mpsc_stream_anchor<T: serde::Serialize>(
&self,
handle: StreamAnchorHandle,
) -> Result<crate::streaming::mpsc::MpscStreamSender<T>, AttachError> {
if handle.is_spsc_stream() {
return Err(AttachError::WrongHandleKind {
handle,
expected: crate::streaming::handle::AnchorKind::Mpsc,
});
}
let (handle_worker_id, local_id) = handle.unpack();
if handle_worker_id != self.worker_id {
return self.attach_mpsc_remote::<T>(handle).await;
}
use dashmap::mapref::entry::Entry;
let (
sender_id,
frame_tx,
heartbeat_interval,
cancel_token,
poison_tx,
poison_rx,
sender_stream_id,
) = match self.mpsc_registry.entry(local_id) {
Entry::Vacant(_) => return Err(AttachError::AnchorNotFound { handle }),
Entry::Occupied(mut occ) => {
let entry = occ.get_mut();
if let Some(limit) = entry.max_senders
&& entry.senders.len() >= limit
{
return Err(AttachError::MaxSendersReached { handle, limit });
}
let sender_id = entry.next_sender_id;
entry.next_sender_id += 1;
if let Some(ref tc) = entry.timeout_cancel {
tc.cancel();
}
entry.timeout_cancel = None;
let frame_tx = entry.frame_tx.clone();
let heartbeat_interval = entry.heartbeat_interval;
let sender_stream_id =
self.next_sender_stream_id.fetch_add(1, Ordering::Relaxed) + 1;
let cancel_token = CancellationToken::new();
let (poison_tx, poison_rx) = flume::bounded::<()>(1);
let slot = crate::streaming::mpsc::anchor::MpscSenderSlot {
pump_token: None,
stream_cancel_handle: Some(
crate::streaming::control::StreamCancelHandle::pack(
self.worker_id,
sender_stream_id,
),
),
};
entry.senders.insert(sender_id, slot);
(
sender_id,
frame_tx,
heartbeat_interval,
cancel_token,
poison_tx,
poison_rx,
sender_stream_id,
)
}
};
let sender_entry = crate::streaming::control::SenderEntry {
cancel_token: cancel_token.clone(),
rx_closer: std::sync::Mutex::new(Some(poison_rx)),
};
self.sender_registry
.senders
.insert(sender_stream_id, sender_entry);
Ok(crate::streaming::mpsc::MpscStreamSender::new(
crate::streaming::mpsc::SenderId(sender_id),
crate::streaming::mpsc::sender::SenderChannel::Local(frame_tx),
handle,
self.mpsc_registry.clone(),
crate::streaming::sender::StreamSenderCancelInfo {
cancel_token,
sender_stream_id,
sender_registry: self.sender_registry.clone(),
poison_tx,
},
heartbeat_interval,
self.metrics.clone(),
))
}
async fn attach_mpsc_remote<T: serde::Serialize>(
&self,
handle: StreamAnchorHandle,
) -> Result<crate::streaming::mpsc::MpscStreamSender<T>, AttachError> {
let (handle_worker_id, _) = handle.unpack();
let messenger = self.messenger_lock.get().ok_or_else(|| {
AttachError::TransportError(anyhow::anyhow!(
"register_handlers not called — messenger unavailable for remote mpsc attach"
))
})?;
let sender_stream_id = self.next_sender_stream_id.fetch_add(1, Ordering::Relaxed) + 1;
let cancel_token = CancellationToken::new();
let (poison_tx, poison_rx) = flume::bounded::<()>(1);
let stream_cancel_handle =
crate::streaming::control::StreamCancelHandle::pack(self.worker_id, sender_stream_id);
let req = crate::streaming::mpsc::control::MpscAnchorAttachRequest {
handle,
session_id: sender_stream_id,
stream_cancel_handle,
supported_transport_keys: self.supported_transport_keys(),
};
let response: crate::streaming::mpsc::control::MpscAnchorAttachResponse = messenger
.typed_unary_streaming::<crate::streaming::mpsc::control::MpscAnchorAttachResponse>(
"_mpsc_anchor_attach",
)
.payload(&req)
.map_err(AttachError::TransportError)?
.worker(handle_worker_id)
.send()
.await
.map_err(AttachError::TransportError)?;
match response {
crate::streaming::mpsc::control::MpscAnchorAttachResponse::Ok {
streaming_transport_key,
heartbeat_interval_ms,
sender_id,
routing_session_id,
initial_credit,
slot_byte_budget,
} => {
let (_, local_id) = handle.unpack();
let connect_session_id = if routing_session_id != 0 {
routing_session_id
} else {
sender_stream_id
};
let frame_tx = self
.connect_streaming(
&streaming_transport_key,
handle_worker_id,
local_id,
connect_session_id,
initial_credit,
slot_byte_budget,
)
.await?;
let sender_entry = crate::streaming::control::SenderEntry {
cancel_token: cancel_token.clone(),
rx_closer: std::sync::Mutex::new(Some(poison_rx)),
};
self.sender_registry
.senders
.insert(sender_stream_id, sender_entry);
Ok(crate::streaming::mpsc::MpscStreamSender::new(
crate::streaming::mpsc::SenderId(sender_id),
crate::streaming::mpsc::sender::SenderChannel::Remote(frame_tx),
handle,
self.mpsc_registry.clone(),
crate::streaming::sender::StreamSenderCancelInfo {
cancel_token,
sender_stream_id,
sender_registry: self.sender_registry.clone(),
poison_tx,
},
Duration::from_millis(heartbeat_interval_ms),
self.metrics.clone(),
))
}
crate::streaming::mpsc::control::MpscAnchorAttachResponse::Err { reason } => {
Err(AttachError::TransportError(anyhow::anyhow!("{}", reason)))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::streaming::frame::{StreamError, StreamFrame};
use anyhow::Result as AnyhowResult;
use futures::StreamExt;
use futures::future::BoxFuture;
use std::sync::Arc;
struct MockTransport;
impl crate::streaming::transport::FrameTransport for MockTransport {
fn key(&self) -> velo_ext::TransportKey {
velo_ext::TransportKey::new("mock-stream")
}
fn address(&self) -> velo_ext::WorkerAddress {
velo_ext::WorkerAddress::empty()
}
fn bind(
&self,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<flume::Receiver<Vec<u8>>>> {
Box::pin(async { Ok(flume::bounded::<Vec<u8>>(256).1) })
}
fn connect(
&self,
_peer: velo_ext::WorkerId,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<flume::Sender<Vec<u8>>>> {
Box::pin(async { Ok(flume::bounded::<Vec<u8>>(256).0) })
}
}
fn make_manager() -> AnchorManager {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport = Arc::new(MockTransport);
AnchorManager::new(worker_id, transport)
}
#[test]
fn test_create_anchor_monotonic_ids() {
let mgr = make_manager();
let a1 = mgr.create_anchor::<u8>();
let a2 = mgr.create_anchor::<u8>();
let a3 = mgr.create_anchor::<u8>();
let (_, id1) = a1.handle().unpack();
let (_, id2) = a2.handle().unpack();
let (_, id3) = a3.handle().unpack();
assert_eq!(id1, 1, "first local_id must be 1");
assert_eq!(id2, 2, "second local_id must be 2");
assert_eq!(id3, 3, "third local_id must be 3");
}
#[test]
fn test_create_anchor_registry_insert() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u8>();
let (_, local_id) = anchor.handle().unpack();
assert!(
mgr.registry.contains_key(&local_id),
"entry must be present in registry after create_anchor"
);
}
#[test]
fn test_exclusive_attach() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u8>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
let result1 = mgr.try_attach(local_id, handle);
assert!(result1.is_ok(), "first attach must succeed: {result1:?}");
let result2 = mgr.try_attach(local_id, handle);
match result2 {
Err(AttachError::AlreadyAttached { .. }) => {}
other => panic!("expected AlreadyAttached, got {other:?}"),
}
let was_attached = mgr.detach(local_id);
assert!(was_attached, "detach must return true when attached");
let result3 = mgr.try_attach(local_id, handle);
assert!(
result3.is_ok(),
"third attach after detach must succeed: {result3:?}"
);
}
#[test]
fn test_cancel_token_idempotent() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u8>();
let (_, local_id) = anchor.handle().unpack();
let token = mgr
.registry
.get(&local_id)
.map(|e| e.cancel_token.clone())
.expect("entry must exist");
token.cancel();
assert!(
token.is_cancelled(),
"token must be cancelled after first cancel()"
);
token.cancel();
assert!(
token.is_cancelled(),
"token must still be cancelled after second cancel()"
);
}
#[test]
fn test_registry_cleanup() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u8>();
let (_, local_id) = anchor.handle().unpack();
assert!(
mgr.registry.contains_key(&local_id),
"entry must exist before cleanup"
);
let removed = mgr.remove_anchor(local_id);
assert!(removed.is_some(), "remove_anchor must return the entry");
assert!(
!mgr.registry.contains_key(&local_id),
"entry must be absent after remove_anchor"
);
}
#[tokio::test]
async fn test_attach_stream_anchor_success() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let result = mgr.attach_stream_anchor::<u32>(handle).await;
assert!(
result.is_ok(),
"attach_stream_anchor should succeed: {:?}",
result.err()
);
let sender = result.unwrap();
sender.finalize().expect("finalize should succeed");
}
#[tokio::test]
async fn test_attach_stream_anchor_not_found() {
let mgr = make_manager();
let fake_handle = crate::streaming::handle::StreamAnchorHandle::pack(
velo_ext::WorkerId::from_u64(42),
999,
);
let result = mgr.attach_stream_anchor::<u32>(fake_handle).await;
match result {
Err(AttachError::AnchorNotFound { .. }) => {}
other => panic!("expected AnchorNotFound, got {:?}", other),
}
}
#[tokio::test]
async fn test_attach_stream_anchor_already_attached() {
let mgr = make_manager();
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let sender1 = mgr
.attach_stream_anchor::<u32>(handle)
.await
.expect("first attach should succeed");
let result = mgr.attach_stream_anchor::<u32>(handle).await;
match result {
Err(AttachError::AlreadyAttached { .. }) => {}
other => panic!("expected AlreadyAttached, got {:?}", other),
}
drop(sender1);
}
#[tokio::test]
async fn test_attach_stream_anchor_sender_can_send() {
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 should succeed");
sender.send(42u32).await.expect("send should succeed");
let result = anchor.next().await;
match result {
Some(Ok(StreamFrame::Item(val))) => assert_eq!(val, 42),
other => panic!("expected Item(42), got {:?}", other),
}
drop(sender);
}
fn make_test_stream<T>() -> (flume::Sender<Vec<u8>>, StreamAnchor<T>) {
let mgr = make_manager();
let anchor = mgr.create_anchor::<T>();
let (_, local_id) = anchor.handle().unpack();
let frame_tx = mgr
.registry
.get(&local_id)
.map(|e| e.frame_tx.clone())
.expect("entry must exist");
(frame_tx, anchor)
}
#[tokio::test]
async fn test_stream_yields_item() {
let (tx, mut stream) = make_test_stream::<u32>();
let bytes = rmp_serde::to_vec(&StreamFrame::Item(42u32)).unwrap();
tx.send(bytes).unwrap();
let result = stream.next().await;
match result {
Some(Ok(StreamFrame::Item(val))) => assert_eq!(val, 42),
other => panic!("expected Some(Ok(Item(42))), got {:?}", other),
}
}
#[tokio::test]
async fn test_stream_yields_sender_error_and_continues() {
let (tx, mut stream) = make_test_stream::<u32>();
let err_bytes =
rmp_serde::to_vec(&StreamFrame::<u32>::SenderError("oops".to_string())).unwrap();
tx.send(err_bytes).unwrap();
let result = stream.next().await;
match result {
Some(Err(StreamError::SenderError(msg))) => assert_eq!(msg, "oops"),
other => panic!("expected SenderError, got {:?}", other),
}
let item_bytes = rmp_serde::to_vec(&StreamFrame::Item(99u32)).unwrap();
tx.send(item_bytes).unwrap();
let result2 = stream.next().await;
match result2 {
Some(Ok(StreamFrame::Item(val))) => assert_eq!(val, 99),
other => panic!("expected Item(99) after SenderError, got {:?}", other),
}
}
#[tokio::test]
async fn test_stream_finalized_then_none() {
let (tx, mut stream) = make_test_stream::<u32>();
let bytes = rmp_serde::to_vec(&StreamFrame::<u32>::Finalized).unwrap();
tx.send(bytes).unwrap();
let result = stream.next().await;
assert!(
matches!(result, Some(Ok(StreamFrame::Finalized))),
"expected Finalized, got {:?}",
result
);
let result2 = stream.next().await;
assert!(
result2.is_none(),
"expected None after Finalized, got {:?}",
result2
);
}
#[tokio::test]
async fn test_stream_detached_then_none() {
let (tx, mut stream) = make_test_stream::<u32>();
let bytes = rmp_serde::to_vec(&StreamFrame::<u32>::Detached).unwrap();
tx.send(bytes).unwrap();
let result = stream.next().await;
assert!(
matches!(result, Some(Ok(StreamFrame::Detached))),
"expected Detached, got {:?}",
result
);
let bytes = rmp_serde::to_vec(&StreamFrame::<u32>::Dropped).unwrap();
tx.send(bytes).unwrap();
let result2 = stream.next().await;
assert!(
matches!(result2, Some(Err(StreamError::SenderDropped))),
"expected SenderDropped after Detached, got {:?}",
result2
);
let result3 = stream.next().await;
assert!(
result3.is_none(),
"expected None after SenderDropped, got {:?}",
result3
);
}
#[tokio::test]
async fn test_stream_dropped_then_none() {
let (tx, mut stream) = make_test_stream::<u32>();
let bytes = rmp_serde::to_vec(&StreamFrame::<u32>::Dropped).unwrap();
tx.send(bytes).unwrap();
let result = stream.next().await;
match result {
Some(Err(StreamError::SenderDropped)) => {}
other => panic!("expected SenderDropped, got {:?}", other),
}
let result2 = stream.next().await;
assert!(
result2.is_none(),
"expected None after Dropped, got {:?}",
result2
);
}
#[tokio::test]
async fn test_stream_transport_error_then_none() {
let (tx, mut stream) = make_test_stream::<u32>();
let bytes = rmp_serde::to_vec(&StreamFrame::<u32>::TransportError(
"conn reset".to_string(),
))
.unwrap();
tx.send(bytes).unwrap();
let result = stream.next().await;
match result {
Some(Err(StreamError::TransportError(msg))) => assert_eq!(msg, "conn reset"),
other => panic!("expected TransportError, got {:?}", other),
}
let result2 = stream.next().await;
assert!(
result2.is_none(),
"expected None after TransportError, got {:?}",
result2
);
}
#[tokio::test]
async fn test_stream_filters_heartbeat() {
let (tx, mut stream) = make_test_stream::<u32>();
let hb_bytes = rmp_serde::to_vec(&StreamFrame::<u32>::Heartbeat).unwrap();
tx.send(hb_bytes).unwrap();
let item_bytes = rmp_serde::to_vec(&StreamFrame::Item(7u32)).unwrap();
tx.send(item_bytes).unwrap();
let result = stream.next().await;
match result {
Some(Ok(StreamFrame::Item(val))) => assert_eq!(val, 7),
other => panic!("expected Item(7) (heartbeat filtered), got {:?}", other),
}
}
#[tokio::test]
async fn test_stream_deserialization_error_then_none() {
let (tx, mut stream) = make_test_stream::<u32>();
tx.send(vec![0xFF, 0xFE, 0xFD]).unwrap();
let result = stream.next().await;
match result {
Some(Err(StreamError::DeserializationError(_))) => {}
other => panic!("expected DeserializationError, got {:?}", other),
}
let result2 = stream.next().await;
assert!(
result2.is_none(),
"expected None after DeserializationError, got {:?}",
result2
);
}
#[tokio::test]
async fn test_stream_none_when_sender_dropped() {
let mgr = make_manager();
let mut stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
let entry = mgr.remove_anchor(local_id);
drop(entry);
let result = stream.next().await;
assert!(
result.is_none(),
"expected None when channel sender dropped, got {:?}",
result
);
}
#[tokio::test]
async fn test_cancel_removes_anchor_from_registry() {
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
assert!(
mgr.registry.contains_key(&local_id),
"anchor must exist before cancel"
);
stream.cancel();
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after cancel(self)"
);
}
#[test]
fn test_builder_creates_manager_no_timeout() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.build()
.expect("builder with required fields should succeed");
assert!(
mgr.default_unattached_timeout.is_none(),
"default_unattached_timeout must be None when not set"
);
}
#[test]
fn test_builder_creates_manager_with_timeout() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(10))
.build()
.expect("builder with timeout should succeed");
assert_eq!(
mgr.default_unattached_timeout,
Some(std::time::Duration::from_secs(10)),
"default_unattached_timeout must match configured value"
);
}
#[test]
fn test_convenience_new_still_works() {
let mgr = make_manager();
assert!(
mgr.default_unattached_timeout.is_none(),
"AnchorManager::new must produce None default_unattached_timeout"
);
}
#[tokio::test]
async fn test_timeout_removes_unattached_anchor() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(1))
.build()
.expect("builder should succeed");
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
assert!(
mgr.registry.contains_key(&local_id),
"anchor must exist after create"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after timeout expires"
);
}
#[tokio::test]
async fn test_expired_anchor_returns_not_found() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(1))
.build()
.expect("builder should succeed");
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let result = mgr.try_attach(local_id, handle);
match result {
Err(AttachError::AnchorNotFound { .. }) => {}
other => panic!("expected AnchorNotFound after timeout, got {:?}", other),
}
}
#[tokio::test]
async fn test_timeout_pauses_on_attach_resumes_on_detach() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(2))
.build()
.expect("builder should succeed");
let anchor = mgr.create_anchor::<u32>();
let handle = anchor.handle();
let (_, local_id) = handle.unpack();
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
assert!(
mgr.registry.contains_key(&local_id),
"anchor must exist before timeout"
);
mgr.try_attach(local_id, handle)
.expect("attach should succeed");
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
assert!(
mgr.registry.contains_key(&local_id),
"anchor must still exist while attached (timeout paused)"
);
mgr.detach(local_id);
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after detach + timeout"
);
}
#[tokio::test]
async fn test_set_timeout_starts_timeout_on_no_default() {
tokio::time::pause();
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
stream.set_timeout(Some(std::time::Duration::from_secs(1)));
assert!(
mgr.registry.contains_key(&local_id),
"anchor must exist before timeout"
);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after set_timeout expires"
);
}
#[tokio::test]
async fn test_set_timeout_none_disables_timeout() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(2))
.build()
.expect("builder should succeed");
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
stream.set_timeout(None);
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
assert!(
mgr.registry.contains_key(&local_id),
"anchor must still exist after disabling timeout"
);
}
#[tokio::test]
async fn test_set_timeout_overrides_default() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(10))
.build()
.expect("builder should succeed");
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
stream.set_timeout(Some(std::time::Duration::from_secs(1)));
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed by overridden 1s timeout, not waiting for 10s default"
);
}
#[tokio::test]
async fn test_set_timeout_while_attached_no_immediate_effect() {
tokio::time::pause();
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let handle = stream.handle();
let (_, local_id) = handle.unpack();
mgr.try_attach(local_id, handle)
.expect("attach should succeed");
stream.set_timeout(Some(std::time::Duration::from_secs(1)));
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
assert!(
mgr.registry.contains_key(&local_id),
"anchor must still exist while attached even with set_timeout"
);
mgr.detach(local_id);
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after detach with stored set_timeout duration"
);
}
#[test]
fn test_builder_with_external_registry() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let external_registry: Arc<DashMap<u64, AnchorEntry>> = Arc::new(DashMap::new());
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.registry(external_registry.clone())
.build()
.expect("builder with external registry should succeed");
assert!(
Arc::ptr_eq(&mgr.registry, &external_registry),
"manager must use the externally provided registry Arc"
);
}
#[test]
fn test_builder_without_registry_creates_own() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.build()
.expect("builder without registry should succeed");
assert_eq!(mgr.registry.len(), 0, "auto-created registry must be empty");
}
#[test]
fn test_create_anchor_inserts_into_shared_registry() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let shared_registry: Arc<DashMap<u64, AnchorEntry>> = Arc::new(DashMap::new());
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.registry(shared_registry.clone())
.build()
.expect("builder should succeed");
assert_eq!(
shared_registry.len(),
0,
"shared registry must be empty before create_anchor"
);
let anchor = mgr.create_anchor::<u32>();
let (_, local_id) = anchor.handle().unpack();
assert_eq!(
shared_registry.len(),
1,
"shared registry must have 1 entry after create_anchor"
);
assert!(
shared_registry.contains_key(&local_id),
"shared registry must contain the created anchor"
);
}
#[test]
fn test_controller_clone() {
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
let ctrl1 = stream.controller();
let ctrl2 = ctrl1.clone();
ctrl2.cancel();
assert!(
!mgr.registry.contains_key(&local_id),
"ctrl2.cancel() must remove anchor from registry"
);
ctrl1.cancel();
}
#[test]
fn test_cancel_self_removes_registry() {
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
assert!(
mgr.registry.contains_key(&local_id),
"anchor must exist before cancel"
);
stream.cancel();
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed after cancel(self)"
);
}
#[test]
fn test_controller_cancel_removes_registry() {
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
let ctrl = stream.controller();
drop(stream);
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be removed by Drop"
);
ctrl.cancel();
}
#[test]
fn test_double_cancel_idempotent() {
let mgr = make_manager();
let stream = mgr.create_anchor::<u32>();
let (_, local_id) = stream.handle().unpack();
let ctrl = stream.controller();
ctrl.cancel();
assert!(
!mgr.registry.contains_key(&local_id),
"anchor must be absent after first cancel"
);
ctrl.cancel();
}
#[test]
fn test_register_handlers_stores_messenger_in_lock() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let messenger = crate::messenger::Messenger::builder()
.build()
.await
.expect("messenger");
let worker_id = velo_ext::WorkerId::from_u64(99);
let transport = Arc::new(MockTransport);
let am = Arc::new(AnchorManager::new(worker_id, transport));
am.register_handlers(Arc::clone(&messenger))
.expect("first register_handlers must succeed");
assert!(
am.messenger_lock.get().is_some(),
"messenger_lock must be Some after register_handlers"
);
});
}
#[test]
fn test_register_handlers_second_call_errors() {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let m1 = crate::messenger::Messenger::builder()
.build()
.await
.unwrap();
let m2 = crate::messenger::Messenger::builder()
.build()
.await
.unwrap();
let worker_id = velo_ext::WorkerId::from_u64(100);
let transport = Arc::new(MockTransport);
let am = Arc::new(AnchorManager::new(worker_id, transport));
am.register_handlers(Arc::clone(&m1))
.expect("first call ok");
let result = am.register_handlers(Arc::clone(&m2));
assert!(result.is_err(), "second call must return Err");
});
}
struct NoopTransport;
impl crate::streaming::transport::FrameTransport for NoopTransport {
fn key(&self) -> velo_ext::TransportKey {
velo_ext::TransportKey::new("noop-stream")
}
fn address(&self) -> velo_ext::WorkerAddress {
velo_ext::WorkerAddress::empty()
}
fn bind(
&self,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<flume::Receiver<Vec<u8>>>> {
Box::pin(async { Ok(flume::bounded(1).1) })
}
fn connect(
&self,
_peer: velo_ext::WorkerId,
_anchor_id: u64,
_session_id: u64,
) -> BoxFuture<'_, AnyhowResult<flume::Sender<Vec<u8>>>> {
Box::pin(async { Ok(flume::bounded(1).0) })
}
}
#[test]
fn test_transport_registry_resolution() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let default_transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let tcp_transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(NoopTransport);
let mut registry = HashMap::new();
registry.insert("noop-stream".to_string(), Arc::clone(&tcp_transport));
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(default_transport)
.transport_registry(Arc::new(registry))
.build()
.expect("builder should succeed");
let resolved = mgr
.resolve_transport(&velo_ext::TransportKey::new("noop-stream"))
.expect("noop-stream key must resolve");
assert!(
Arc::ptr_eq(&resolved, &tcp_transport),
"resolved transport must be the registered noop transport"
);
let err = match mgr.resolve_transport(&velo_ext::TransportKey::new("missing-stream")) {
Err(e) => e,
Ok(_) => panic!("unregistered key in non-empty registry must error"),
};
let msg = format!("{}", err);
assert!(
msg.contains("unsupported streaming transport key"),
"error message must mention unsupported key, got: {}",
msg
);
}
#[test]
fn test_unsupported_key() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let default_transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mut registry = HashMap::new();
registry.insert(
"noop-stream".to_string(),
Arc::new(NoopTransport) as Arc<dyn crate::streaming::transport::FrameTransport>,
);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(default_transport)
.transport_registry(Arc::new(registry))
.build()
.expect("builder should succeed");
let err = match mgr.resolve_transport(&velo_ext::TransportKey::new("unknown")) {
Err(e) => e,
Ok(_) => panic!("unknown key must return error"),
};
let msg = format!("{}", err);
assert!(
msg.contains("unknown"),
"error must name the unsupported key, got: {}",
msg
);
}
#[test]
fn test_empty_registry_fallback() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let default_transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let default_clone = Arc::clone(&default_transport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(default_transport)
.build()
.expect("builder should succeed");
let resolved = mgr
.resolve_transport(&velo_ext::TransportKey::new("anything"))
.expect("empty registry must fall back to default transport");
assert!(
Arc::ptr_eq(&resolved, &default_clone),
"resolved transport must be the default transport when registry is empty"
);
}
#[test]
fn test_default_heartbeat_interval_is_5s() {
let mgr = make_manager();
assert_eq!(
mgr.default_heartbeat_interval,
std::time::Duration::from_secs(5),
"AnchorManager::new must default heartbeat_interval to 5s"
);
}
#[test]
fn test_builder_overrides_default_heartbeat_interval() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_heartbeat_interval(std::time::Duration::from_millis(750))
.build()
.expect("builder should succeed");
assert_eq!(
mgr.default_heartbeat_interval,
std::time::Duration::from_millis(750),
"builder must accept default_heartbeat_interval override"
);
}
#[test]
fn test_create_anchor_uses_manager_heartbeat_default() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_heartbeat_interval(std::time::Duration::from_millis(250))
.build()
.expect("builder should succeed");
let anchor = mgr.create_anchor::<u32>();
let (_, local_id) = anchor.handle().unpack();
let entry = mgr
.registry
.get(&local_id)
.expect("entry must exist after create_anchor");
assert_eq!(
entry.heartbeat_interval,
std::time::Duration::from_millis(250),
"create_anchor must inherit manager-level default_heartbeat_interval"
);
}
#[test]
fn test_create_anchor_with_config_overrides_heartbeat() {
let mgr = make_manager(); let cfg = AnchorConfig {
unattached_timeout: None,
heartbeat_interval: Some(std::time::Duration::from_millis(123)),
};
let anchor = mgr.create_anchor_with_config::<u32>(cfg);
let (_, local_id) = anchor.handle().unpack();
let entry = mgr.registry.get(&local_id).expect("entry exists");
assert_eq!(
entry.heartbeat_interval,
std::time::Duration::from_millis(123),
"AnchorConfig::heartbeat_interval must override the manager default"
);
}
#[tokio::test]
async fn test_create_anchor_with_config_overrides_unattached_timeout() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(10))
.build()
.expect("builder should succeed");
let cfg = AnchorConfig {
unattached_timeout: Some(std::time::Duration::from_millis(50)),
heartbeat_interval: None,
};
let anchor = mgr.create_anchor_with_config::<u32>(cfg);
let (_, local_id) = anchor.handle().unpack();
let entry = mgr.registry.get(&local_id).expect("entry exists");
assert_eq!(
entry.unattached_timeout,
Some(std::time::Duration::from_millis(50)),
"AnchorConfig::unattached_timeout must override the manager default"
);
}
#[tokio::test]
async fn test_create_anchor_with_default_config_inherits_both() {
let worker_id = velo_ext::WorkerId::from_u64(42);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.default_unattached_timeout(std::time::Duration::from_secs(7))
.default_heartbeat_interval(std::time::Duration::from_millis(800))
.build()
.expect("builder should succeed");
let anchor = mgr.create_anchor_with_config::<u32>(AnchorConfig::default());
let (_, local_id) = anchor.handle().unpack();
let entry = mgr.registry.get(&local_id).expect("entry exists");
assert_eq!(
entry.heartbeat_interval,
std::time::Duration::from_millis(800)
);
assert_eq!(
entry.unattached_timeout,
Some(std::time::Duration::from_secs(7))
);
}
#[tokio::test]
async fn test_per_anchor_heartbeat_propagates_through_attach_response() {
tokio::time::pause();
let worker_id = velo_ext::WorkerId::from_u64(7);
let transport: Arc<dyn crate::streaming::transport::FrameTransport> =
Arc::new(MockTransport);
let mgr = AnchorManagerBuilder::default()
.worker_id(worker_id)
.transport(transport)
.build()
.expect("builder should succeed");
let cfg = AnchorConfig {
unattached_timeout: None,
heartbeat_interval: Some(std::time::Duration::from_millis(200)),
};
let anchor = mgr.create_anchor_with_config::<u32>(cfg);
let handle = anchor.handle();
let sender = mgr
.attach_stream_anchor::<u32>(handle)
.await
.expect("local attach should succeed");
let collected: Arc<DashMap<usize, crate::streaming::frame::StreamFrame<u32>>> =
Arc::new(DashMap::new());
let collected_clone = collected.clone();
tokio::spawn(async move {
use futures::StreamExt;
let mut anchor = anchor;
let mut idx = 0usize;
while let Some(frame) = anchor.next().await {
if let Ok(f) = frame {
collected_clone.insert(idx, f);
idx += 1;
}
}
});
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
let (_, local_id) = handle.unpack();
let entry = mgr.registry.get(&local_id).expect("entry exists");
assert_eq!(
entry.heartbeat_interval,
std::time::Duration::from_millis(200),
"AnchorEntry must store the per-anchor cadence after attach"
);
drop(sender);
}
}