pub(crate) mod flow_control;
pub(crate) mod ingress;
pub(crate) mod peer_batcher;
pub(crate) mod protocol;
#[cfg(test)]
mod tests;
use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::time::Duration;
use anyhow::{Result, anyhow};
use dashmap::DashMap;
use futures::future::BoxFuture;
use tokio_util::sync::CancellationToken;
use velo_ext::{TransportKey, WorkerAddress, WorkerId};
use self::flow_control::{DEFAULT_PEER_BYTE_BUDGET, DEFAULT_SLOT_BYTE_BUDGET, NegotiatedLimits};
use self::ingress::IngressRegistry;
use self::peer_batcher::{
BatcherContext, BatcherHandle, BatcherMap, OpenRejected, OpenSlotRequest,
};
use crate::messenger::{Context, Handler, Messenger};
use crate::observability::{MuxMetricsHandle, VeloMetrics};
use crate::streaming::transport::FrameTransport;
pub const MESSENGER_MUX_KEY: &str = "messenger-mux-v1";
pub(crate) const STREAM_BATCH_HANDLER: &str = "_stream_batch";
const ACCEPT_TIMEOUT: Duration = Duration::from_secs(60);
const CONNECT_ATTEMPTS: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AutoFlush {
pub on_admission: bool,
pub max_linger: Option<Duration>,
}
impl Default for AutoFlush {
fn default() -> Self {
Self {
on_admission: true,
max_linger: None,
}
}
}
impl AutoFlush {
#[must_use]
pub const fn with_max_linger(mut self, window: Duration) -> Self {
self.max_linger = Some(window);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FlushPolicy {
Auto(AutoFlush),
Manual,
}
impl Default for FlushPolicy {
fn default() -> Self {
Self::Auto(AutoFlush::default())
}
}
impl FlushPolicy {
pub(crate) const fn max_linger(self) -> Option<Duration> {
match self {
Self::Auto(auto) => auto.max_linger,
Self::Manual => None,
}
}
pub(crate) const fn on_admission(self) -> bool {
match self {
Self::Auto(auto) => auto.on_admission,
Self::Manual => false,
}
}
}
#[derive(Debug, Clone)]
pub struct MuxConfig {
pub enabled: bool,
pub max_batch_bytes: usize,
pub initial_credit: u32,
pub slot_byte_budget: u32,
pub peer_byte_budget: u64,
pub credit_sweep_interval: Duration,
pub batcher_idle_ttl: Duration,
pub flush_policy: FlushPolicy,
}
impl Default for MuxConfig {
fn default() -> Self {
Self {
enabled: false,
max_batch_bytes: 60 * 1024,
initial_credit: 256,
slot_byte_budget: DEFAULT_SLOT_BYTE_BUDGET,
peer_byte_budget: DEFAULT_PEER_BYTE_BUDGET,
credit_sweep_interval: Duration::from_millis(2),
batcher_idle_ttl: Duration::from_secs(60),
flush_policy: FlushPolicy::Auto(AutoFlush::default()),
}
}
}
impl MuxConfig {
fn idle_ticks(&self) -> u32 {
let interval = self.credit_sweep_interval.as_millis().max(1);
let ttl = self.batcher_idle_ttl.as_millis();
u32::try_from(ttl / interval).unwrap_or(u32::MAX).max(1)
}
}
pub(crate) struct MessengerMuxTransport {
core: Arc<MuxCore>,
key: TransportKey,
}
struct MuxCore {
messenger: Arc<Messenger>,
config: MuxConfig,
limits: NegotiatedLimits,
metrics: Option<MuxMetricsHandle>,
batchers: Arc<BatcherMap>,
ingress: Arc<IngressRegistry>,
epochs: Arc<AtomicU64>,
cancel: CancellationToken,
}
impl MessengerMuxTransport {
pub(crate) fn new(
messenger: Arc<Messenger>,
config: MuxConfig,
metrics: Option<Arc<VeloMetrics>>,
) -> Result<Arc<Self>> {
let limits = NegotiatedLimits::from_wire(config.initial_credit, config.slot_byte_budget)
.map_err(|error| anyhow!("messenger mux: {error}"))?;
let config = MuxConfig {
slot_byte_budget: limits.slot_byte_budget(),
..config
};
let core = Arc::new(MuxCore {
messenger: Arc::clone(&messenger),
config,
limits,
metrics: metrics.as_ref().map(|metrics| metrics.bind_mux()),
batchers: Arc::new(DashMap::new()),
ingress: Arc::new(IngressRegistry::default()),
epochs: Arc::new(AtomicU64::new(1)),
cancel: CancellationToken::new(),
});
let handler_core = Arc::downgrade(&core);
let handler = Handler::am_handler_async(STREAM_BATCH_HANDLER, move |ctx: Context| {
let handler_core = handler_core.clone();
async move {
if let Some(core) = handler_core.upgrade() {
core.deliver_batch(ctx.sender_worker_id(), &ctx.payload);
}
Ok(())
}
})
.ordered()
.build();
messenger.register_streaming_handler(handler)?;
spawn_sweep(&core);
Ok(Arc::new(Self {
core,
key: TransportKey::new(MESSENGER_MUX_KEY),
}))
}
}
impl MuxCore {
fn batcher(&self, peer: WorkerId) -> Arc<BatcherHandle> {
if let Some(existing) = self.batchers.get(&peer) {
return Arc::clone(existing.value());
}
Arc::clone(
self.batchers
.entry(peer)
.or_insert_with(|| {
peer_batcher::spawn(
peer,
BatcherContext {
messenger: Arc::clone(&self.messenger),
config: self.config.clone(),
metrics: self.metrics.clone(),
epochs: Arc::clone(&self.epochs),
batchers: Arc::clone(&self.batchers),
cancel: self.cancel.clone(),
#[cfg(test)]
hooks: None,
},
)
})
.value(),
)
}
fn deliver_batch(&self, peer: WorkerId, payload: &bytes::Bytes) {
let outcome = ingress::handle_batch(
&self.ingress,
&self.config,
self.metrics.as_ref(),
peer,
payload,
);
if let Some(metrics) = &self.metrics {
for _ in 0..outcome.opened {
metrics.slot_opened();
}
for _ in 0..outcome.closed {
metrics.slot_closed();
}
}
if outcome.replies.is_empty() && outcome.grants.is_empty() && outcome.peer_closes.is_empty()
{
return;
}
let batcher = self.batcher(peer);
for (slot, delta) in outcome.grants {
batcher.grant(slot, delta);
}
for (slot, reason) in outcome.peer_closes {
batcher.peer_closed(slot, reason);
}
if !outcome.replies.is_empty() {
self.send_replies(&batcher, peer, &outcome.replies);
}
}
fn flush_batches(&self) {
for entry in self.batchers.iter() {
entry.value().kick_flush();
}
}
fn send_replies(
&self,
batcher: &Arc<BatcherHandle>,
peer: WorkerId,
replies: &[peer_batcher::ReplyRecord],
) {
if batcher.is_alive() {
batcher.reply(replies);
return;
}
self.batcher(peer).reply(replies);
}
fn sweep(&self) {
for peer in self.ingress.peers() {
let replies = self.ingress.sweep_credit(peer);
if !replies.is_empty() {
let batcher = self.batcher(peer);
self.send_replies(&batcher, peer, &replies);
}
}
let threshold = self.config.idle_ticks();
let peers: Vec<WorkerId> = self.batchers.iter().map(|entry| *entry.key()).collect();
for peer in peers {
let Some(handle) = self.batchers.get(&peer) else {
continue;
};
let idle = handle.tick_idle();
drop(handle);
if idle < threshold || self.ingress.live_slots(peer) > 0 {
continue;
}
if let Some((_, handle)) = self
.batchers
.remove_if(&peer, |_, handle| handle.try_retire(threshold))
{
handle.retire();
}
}
}
}
impl Drop for MuxCore {
fn drop(&mut self) {
self.cancel.cancel();
let closed = self.ingress.shutdown();
if let Some(metrics) = &self.metrics {
for _ in 0..closed {
metrics.slot_closed();
}
}
}
}
fn spawn_sweep(core: &Arc<MuxCore>) {
let weak = Arc::downgrade(core);
let cancel = core.cancel.clone();
let interval = core.config.credit_sweep_interval;
tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
biased;
() = cancel.cancelled() => return,
_ = ticker.tick() => {}
}
let Some(core) = weak.upgrade() else {
return;
};
core.sweep();
}
});
}
impl FrameTransport for MessengerMuxTransport {
fn key(&self) -> TransportKey {
self.key.clone()
}
fn address(&self) -> WorkerAddress {
WorkerAddress::empty()
}
fn bind(
&self,
anchor_id: u64,
session_id: u64,
) -> BoxFuture<'_, Result<flume::Receiver<Vec<u8>>>> {
let core = Arc::clone(&self.core);
Box::pin(async move {
let (frame_tx, frame_rx) = flume::bounded::<Vec<u8>>(core.limits.slot_buffer_depth());
core.ingress.register_bind(anchor_id, session_id, frame_tx);
let expiry = Arc::downgrade(&core);
let cancel = core.cancel.clone();
tokio::spawn(async move {
tokio::select! {
() = cancel.cancelled() => return,
() = tokio::time::sleep(ACCEPT_TIMEOUT) => {}
}
let Some(core) = expiry.upgrade() else {
return;
};
if core.ingress.expire_bind(anchor_id, session_id) {
tracing::warn!(
anchor_id,
session_id,
"messenger mux: no OpenSlot arrived before the accept window closed"
);
}
});
Ok(frame_rx)
})
}
fn connect(
&self,
peer: WorkerId,
anchor_id: u64,
session_id: u64,
) -> BoxFuture<'_, Result<flume::Sender<Vec<u8>>>> {
let limits = self.core.limits;
self.connect_negotiated(peer, anchor_id, session_id, limits)
}
}
impl MessengerMuxTransport {
pub(crate) fn advertised_limits(&self) -> NegotiatedLimits {
self.core.limits
}
pub(crate) fn flush_batches(&self) {
self.core.flush_batches();
}
pub(crate) fn connect_negotiated(
&self,
peer: WorkerId,
anchor_id: u64,
session_id: u64,
limits: NegotiatedLimits,
) -> BoxFuture<'_, Result<flume::Sender<Vec<u8>>>> {
let core = Arc::clone(&self.core);
Box::pin(async move {
for _ in 0..CONNECT_ATTEMPTS {
let batcher = core.batcher(peer);
let (inlet_tx, inlet_rx) = flume::bounded::<Vec<u8>>(limits.slot_buffer_depth());
let (ack_tx, ack_rx) = tokio::sync::oneshot::channel();
if batcher
.open_slot(OpenSlotRequest {
anchor_id,
session_id,
inlet: inlet_rx,
credit: limits.open_credit(),
slot_byte_budget: limits.slot_byte_budget(),
ack: ack_tx,
})
.await
.is_err()
{
continue;
}
match ack_rx.await {
Ok(Ok(())) => return Ok(inlet_tx),
Ok(Err(OpenRejected::Retired)) | Err(_) => continue,
Ok(Err(error)) => return Err(error.into()),
}
}
Err(anyhow!(
"messenger mux: could not open a slot to peer {peer} after {CONNECT_ATTEMPTS} attempts"
))
})
}
}