mod slot;
#[cfg(test)]
mod tests;
use std::sync::Mutex;
use bytes::Bytes;
use dashmap::DashMap;
use velo_ext::WorkerId;
use self::slot::{Applied, IngressSlot, heartbeat_frame};
use super::MuxConfig;
use super::flow_control::ByteBudget;
use super::peer_batcher::ReplyRecord;
use super::protocol::{
BatchDecoder, BatchHeader, CloseReason, Record, RecordBody, SlotId, batch_seq_gap,
};
use crate::observability::{MuxDirection, MuxDropReason, MuxMetricsHandle};
const MAX_INGRESS_SLOTS_PER_PEER: usize = 1 << 16;
struct BindEntry {
frame_tx: flume::Sender<Vec<u8>>,
}
#[derive(Default)]
pub(crate) struct IngressRegistry {
binds: DashMap<(u64, u64), BindEntry>,
peers: DashMap<WorkerId, Mutex<PeerIngress>>,
}
struct PeerIngress {
epoch: Option<u64>,
last_batch_seq: Option<u32>,
slots: Vec<Option<IngressSlot>>,
peer_bytes: ByteBudget,
}
impl PeerIngress {
fn new(peer_byte_budget: u64) -> Self {
Self {
epoch: None,
last_batch_seq: None,
slots: Vec::new(),
peer_bytes: ByteBudget::new(peer_byte_budget),
}
}
fn live(&self) -> usize {
self.slots.iter().filter(|entry| entry.is_some()).count()
}
}
#[derive(Default)]
pub(crate) struct BatchOutcome {
pub(crate) replies: Vec<ReplyRecord>,
pub(crate) grants: Vec<(SlotId, u32)>,
pub(crate) peer_closes: Vec<(SlotId, CloseReason)>,
pub(crate) opened: usize,
pub(crate) closed: usize,
}
struct ApplyCtx<'a> {
registry: &'a IngressRegistry,
config: &'a MuxConfig,
metrics: Option<&'a MuxMetricsHandle>,
}
impl IngressRegistry {
pub(crate) fn register_bind(
&self,
anchor_id: u64,
session_id: u64,
frame_tx: flume::Sender<Vec<u8>>,
) {
self.binds
.insert((anchor_id, session_id), BindEntry { frame_tx });
}
pub(crate) fn expire_bind(&self, anchor_id: u64, session_id: u64) -> bool {
self.binds.remove(&(anchor_id, session_id)).is_some()
}
#[cfg(test)]
pub(crate) fn peer_bytes_used(&self, peer: WorkerId) -> u64 {
self.peers
.get(&peer)
.map_or(0, |entry| lock(entry.value()).peer_bytes.used())
}
pub(crate) fn live_slots(&self, peer: WorkerId) -> usize {
self.peers
.get(&peer)
.map_or(0, |entry| lock(entry.value()).live())
}
pub(crate) fn peers(&self) -> Vec<WorkerId> {
self.peers.iter().map(|entry| *entry.key()).collect()
}
pub(crate) fn sweep_credit(&self, peer: WorkerId) -> Vec<ReplyRecord> {
let Some(entry) = self.peers.get(&peer) else {
return Vec::new();
};
let mut state = lock(entry.value());
let mut replies = Vec::new();
collect_grants(&mut state, &mut replies);
replies
}
pub(crate) fn shutdown(&self) -> usize {
let mut closed = 0;
for entry in self.peers.iter() {
let mut state = lock(entry.value());
closed += retire_epoch(&mut state, None);
}
self.binds.clear();
closed
}
}
pub(crate) fn handle_batch(
registry: &IngressRegistry,
config: &MuxConfig,
metrics: Option<&MuxMetricsHandle>,
peer: WorkerId,
payload: &Bytes,
) -> BatchOutcome {
let mut outcome = BatchOutcome::default();
let header = match BatchHeader::decode(payload) {
Ok(header) => header,
Err(error) => {
tracing::warn!(peer = %peer, %error, "messenger mux: undecodable batch header");
return outcome;
}
};
if !registry.peers.contains_key(&peer) {
registry
.peers
.entry(peer)
.or_insert_with(|| Mutex::new(PeerIngress::new(config.peer_byte_budget)));
}
let Some(entry) = registry.peers.get(&peer) else {
return outcome;
};
let mut state = lock(entry.value());
if !accept_epoch(&mut state, &header, metrics, &mut outcome) {
return outcome;
}
note_batch_seq(&mut state, &header, metrics);
let decoder = match BatchDecoder::new(payload) {
Ok(decoder) => decoder,
Err(error) => {
tracing::warn!(peer = %peer, %error, "messenger mux: undecodable batch");
return outcome;
}
};
if let Some(metrics) = metrics {
metrics.batch(MuxDirection::Received, usize::from(header.record_count));
}
let ctx = ApplyCtx {
registry,
config,
metrics,
};
for decoded in decoder {
match decoded {
Ok(record) => apply_record(&mut state, &ctx, &record, &mut outcome),
Err(error) => {
tracing::warn!(
peer = %peer,
%error,
"messenger mux: malformed record; the rest of the batch is skipped"
);
break;
}
}
}
collect_grants(&mut state, &mut outcome.replies);
outcome
}
fn accept_epoch(
state: &mut PeerIngress,
header: &BatchHeader,
metrics: Option<&MuxMetricsHandle>,
outcome: &mut BatchOutcome,
) -> bool {
match state.epoch {
None => state.epoch = Some(header.peer_epoch),
Some(current) if header.peer_epoch < current => {
if let Some(metrics) = metrics {
metrics.records_dropped(MuxDropReason::StaleEpoch, u64::from(header.record_count));
}
return false;
}
Some(current) if header.peer_epoch > current => {
outcome.closed += retire_epoch(state, metrics);
state.epoch = Some(header.peer_epoch);
state.last_batch_seq = None;
}
Some(_) => {}
}
true
}
fn note_batch_seq(
state: &mut PeerIngress,
header: &BatchHeader,
metrics: Option<&MuxMetricsHandle>,
) {
if let (Some(metrics), Some(last)) = (metrics, state.last_batch_seq) {
let gap = batch_seq_gap(last.wrapping_add(1), header.batch_seq);
if gap > 0 {
metrics.batch_seq_gap(gap);
}
}
state.last_batch_seq = Some(header.batch_seq);
}
fn apply_record(
state: &mut PeerIngress,
ctx: &ApplyCtx<'_>,
record: &Record<'_>,
outcome: &mut BatchOutcome,
) {
match record.body {
RecordBody::OpenSlot {
anchor_id,
session_id,
} => open_slot(state, ctx, record, anchor_id, session_id, outcome),
RecordBody::CreditUpdate { delta } => {
outcome.grants.push((record.slot, delta));
}
RecordBody::CloseSlot { reason } => {
close_slot(state, ctx, record.slot, record.frame_seq, reason, outcome);
}
RecordBody::Data(body) => deliver(state, ctx, record, body.to_vec(), outcome),
RecordBody::SlotHeartbeat => deliver(state, ctx, record, heartbeat_frame(), outcome),
}
}
fn open_slot(
state: &mut PeerIngress,
ctx: &ApplyCtx<'_>,
record: &Record<'_>,
anchor_id: u64,
session_id: u64,
outcome: &mut BatchOutcome,
) {
let id = record.slot;
let index = id.index() as usize;
if index >= MAX_INGRESS_SLOTS_PER_PEER {
outcome.replies.push(ReplyRecord::CloseSlot {
slot: id,
reason: CloseReason::ProtocolError,
});
return;
}
if state
.slots
.get(index)
.and_then(Option::as_ref)
.is_some_and(|incumbent| incumbent.id != id)
{
outcome.replies.push(ReplyRecord::CloseSlot {
slot: id,
reason: CloseReason::ProtocolError,
});
if let Some(metrics) = ctx.metrics {
metrics.record_dropped(MuxDropReason::SlotCollision);
}
return;
}
let Some((_, bind)) = ctx.registry.binds.remove(&(anchor_id, session_id)) else {
outcome.replies.push(ReplyRecord::CloseSlot {
slot: id,
reason: CloseReason::UnknownSlot,
});
if let Some(metrics) = ctx.metrics {
metrics.record_dropped(MuxDropReason::UnknownSlot);
}
return;
};
if state.slots.len() <= index {
state.slots.resize_with(index + 1, || None);
}
if state.slots.get(index).and_then(Option::as_ref).is_some() {
finish_close(state, id, CloseReason::PeerGone, ctx.metrics, outcome);
}
let slot = IngressSlot::new(
id,
bind.frame_tx,
ctx.config.initial_credit,
ctx.config.slot_byte_budget,
record.frame_seq.saturating_add(1),
);
state.slots[index] = Some(slot);
outcome.opened += 1;
}
fn close_slot(
state: &mut PeerIngress,
ctx: &ApplyCtx<'_>,
id: SlotId,
frame_seq: u32,
reason: CloseReason,
outcome: &mut BatchOutcome,
) {
if matches!(
reason,
CloseReason::UnknownSlot | CloseReason::ProtocolError
) {
outcome.peer_closes.push((id, reason));
return;
}
let due = match checked_slot(state, ctx.metrics, id) {
Some(slot) => slot.apply_close(frame_seq, reason),
None => return,
};
if due {
finish_close(state, id, reason, ctx.metrics, outcome);
}
}
fn finish_close(
state: &mut PeerIngress,
id: SlotId,
reason: CloseReason,
metrics: Option<&MuxMetricsHandle>,
outcome: &mut BatchOutcome,
) {
let index = id.index() as usize;
let Some(mut slot) = state.slots.get_mut(index).and_then(Option::take) else {
return;
};
state.peer_bytes.release(slot.hold_bytes_used() as usize);
if let Some(metrics) = metrics
&& slot.held() > 0
{
metrics.held_records_delta(-(slot.held() as i64));
}
if reason != CloseReason::TerminalSent {
slot.inject_dropped();
}
drop(slot);
outcome.closed += 1;
}
fn deliver(
state: &mut PeerIngress,
ctx: &ApplyCtx<'_>,
record: &Record<'_>,
body: Vec<u8>,
outcome: &mut BatchOutcome,
) {
let id = record.slot;
let index = id.index() as usize;
if checked_slot(state, ctx.metrics, id).is_none() {
return;
}
let peer_bytes = &mut state.peer_bytes;
let Some(slot) = state.slots[index].as_mut() else {
return;
};
let held_before = slot.held();
let applied = slot.apply_data(record.frame_seq, body, peer_bytes);
let held_after = slot.held();
let due = slot.due_close();
if let Some(metrics) = ctx.metrics
&& held_after != held_before
{
metrics.held_records_delta(held_after as i64 - held_before as i64);
}
match applied {
Applied::Delivered | Applied::Held => {
if let Some(reason) = due {
finish_close(state, id, reason, ctx.metrics, outcome);
}
}
Applied::Duplicate => {
if let Some(metrics) = ctx.metrics {
metrics.record_dropped(MuxDropReason::Duplicate);
}
}
Applied::ReaderStall => {
if let Some(metrics) = ctx.metrics {
metrics.reader_stall();
}
fail_slot(state, ctx, id, CloseReason::ProtocolError, outcome);
}
Applied::Fault(reason) => {
if let Some(metrics) = ctx.metrics
&& reason == CloseReason::ProtocolError
{
metrics.hold_overflow();
}
fail_slot(state, ctx, id, reason, outcome);
}
}
}
fn fail_slot(
state: &mut PeerIngress,
ctx: &ApplyCtx<'_>,
id: SlotId,
reason: CloseReason,
outcome: &mut BatchOutcome,
) {
finish_close(state, id, reason, ctx.metrics, outcome);
outcome
.replies
.push(ReplyRecord::CloseSlot { slot: id, reason });
}
fn checked_slot<'a>(
state: &'a mut PeerIngress,
metrics: Option<&MuxMetricsHandle>,
id: SlotId,
) -> Option<&'a mut IngressSlot> {
let index = id.index() as usize;
match state.slots.get_mut(index).and_then(Option::as_mut) {
Some(slot) if slot.id == id => Some(slot),
Some(_) => {
if let Some(metrics) = metrics {
metrics.record_dropped(MuxDropReason::Generation);
}
None
}
None => {
if let Some(metrics) = metrics {
metrics.record_dropped(MuxDropReason::ClosedSlot);
}
None
}
}
}
fn retire_epoch(state: &mut PeerIngress, metrics: Option<&MuxMetricsHandle>) -> usize {
let mut closed = 0;
for index in 0..state.slots.len() {
if let Some(mut slot) = state.slots[index].take() {
if let Some(metrics) = metrics
&& slot.held() > 0
{
metrics.held_records_delta(-(slot.held() as i64));
}
slot.inject_dropped();
closed += 1;
}
}
state.slots.clear();
state.peer_bytes = ByteBudget::new(state.peer_bytes.limit());
closed
}
fn collect_grants(state: &mut PeerIngress, replies: &mut Vec<ReplyRecord>) {
for entry in &mut state.slots {
let Some(slot) = entry.as_mut() else {
continue;
};
slot.reconcile();
if let Some(delta) = slot.take_grant() {
replies.push(ReplyRecord::CreditUpdate {
slot: slot.id,
delta,
});
}
}
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}