mod control;
mod flush_gate;
mod records;
mod slot_stream;
#[cfg(test)]
pub(crate) mod test_hooks;
#[cfg(test)]
mod tests;
mod writer;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
use dashmap::DashMap;
use futures::future::FutureExt;
use futures::stream::{SelectAll, StreamExt};
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use velo_ext::WorkerId;
use self::control::{ControlInbox, DrainedControl, OwnedControl, PeerControl};
use self::flush_gate::{FlushGate, linger_until};
pub(crate) use self::slot_stream::AllocError;
use self::slot_stream::{EgressSlots, SlotItem, SlotStream};
#[cfg(test)]
use self::test_hooks::TestHooks;
use self::writer::BatchWriter;
use super::MuxConfig;
use super::protocol::{
BATCH_HEADER_LEN, BatchEncoder, CloseReason, EncodeError, SlotId, record_encoded_len,
};
use crate::messenger::Messenger;
use crate::observability::{MuxDropReason, MuxMetricsHandle};
use crate::streaming::messenger_mux::flow_control::{CreditClass, SlotCredit};
use crate::streaming::sender::is_terminal_sentinel;
pub(crate) type BatcherMap = DashMap<WorkerId, Arc<BatcherHandle>>;
pub(crate) struct OpenSlotRequest {
pub(crate) anchor_id: u64,
pub(crate) session_id: u64,
pub(crate) inlet: flume::Receiver<Vec<u8>>,
pub(crate) credit: SlotCredit,
pub(crate) slot_byte_budget: u32,
pub(crate) ack: oneshot::Sender<Result<(), OpenRejected>>,
}
const OPEN_QUEUE_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReplyRecord {
CreditUpdate { slot: SlotId, delta: u32 },
CloseSlot { slot: SlotId, reason: CloseReason },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(crate) enum OpenRejected {
#[error("peer batcher was retired; retry with a fresh one")]
Retired,
#[error("{0}")]
Alloc(#[from] AllocError),
}
pub(crate) struct BatcherHandle {
opens: flume::Sender<OpenSlotRequest>,
control: Arc<ControlInbox>,
live_slots: AtomicUsize,
idle_ticks: AtomicU32,
retired: AtomicBool,
alive: AtomicBool,
}
impl BatcherHandle {
pub(crate) async fn open_slot(
&self,
request: OpenSlotRequest,
) -> Result<(), flume::SendError<OpenSlotRequest>> {
self.opens.send_async(request).await
}
pub(crate) fn is_alive(&self) -> bool {
self.alive.load(Ordering::Acquire)
}
pub(crate) fn grant(&self, slot: SlotId, delta: u32) {
self.control.grant(slot, delta);
}
pub(crate) fn peer_closed(&self, slot: SlotId, reason: CloseReason) {
self.control.peer_closed(slot, reason);
}
pub(crate) fn reply(&self, records: &[ReplyRecord]) {
for record in records {
match *record {
ReplyRecord::CreditUpdate { slot, delta } => self.control.reply_credit(slot, delta),
ReplyRecord::CloseSlot { slot, reason } => self.control.reply_close(slot, reason),
}
}
}
pub(crate) fn retire(&self) {
self.control.retire();
}
pub(crate) fn kick_flush(&self) {
self.control.kick_flush();
}
#[cfg(test)]
pub(crate) fn pending_control(&self) -> usize {
self.control.pending_len()
}
pub(crate) fn tick_idle(&self) -> u32 {
self.idle_ticks
.fetch_add(1, Ordering::Relaxed)
.saturating_add(1)
}
pub(crate) fn try_retire(&self, idle_threshold: u32) -> bool {
self.live_slots.load(Ordering::Relaxed) == 0
&& self.idle_ticks.load(Ordering::Relaxed) >= idle_threshold
&& !self.retired.swap(true, Ordering::AcqRel)
}
fn mark_active(&self) {
self.idle_ticks.store(0, Ordering::Relaxed);
}
fn is_retired(&self) -> bool {
self.retired.load(Ordering::Acquire)
}
}
pub(crate) struct BatcherContext {
pub(crate) messenger: Arc<Messenger>,
pub(crate) config: MuxConfig,
pub(crate) metrics: Option<MuxMetricsHandle>,
pub(crate) epochs: Arc<AtomicU64>,
pub(crate) batchers: Arc<BatcherMap>,
pub(crate) cancel: CancellationToken,
#[cfg(test)]
pub(crate) hooks: Option<Arc<TestHooks>>,
}
pub(crate) fn spawn(peer: WorkerId, ctx: BatcherContext) -> Arc<BatcherHandle> {
let (opens, open_rx) = flume::bounded(OPEN_QUEUE_DEPTH);
let control = Arc::new(ControlInbox::new(ctx.metrics.clone()));
let handle = Arc::new(BatcherHandle {
opens,
control: Arc::clone(&control),
live_slots: AtomicUsize::new(0),
idle_ticks: AtomicU32::new(0),
retired: AtomicBool::new(false),
alive: AtomicBool::new(true),
});
let epoch = ctx.epochs.fetch_add(1, Ordering::Relaxed);
let gate = FlushGate::new(ctx.config.flush_policy, ctx.metrics.clone());
let writer = BatchWriter::new(
Arc::clone(&ctx.messenger),
peer,
ctx.config,
ctx.metrics.clone(),
epoch,
);
let batcher = Batcher {
peer,
metrics: ctx.metrics,
handle: Arc::clone(&handle),
epochs: ctx.epochs,
batchers: ctx.batchers,
cancel: ctx.cancel,
control,
gate,
writer,
slots: EgressSlots::default(),
streams: SelectAll::new(),
stopping: false,
#[cfg(test)]
hooks: ctx.hooks,
};
tokio::spawn(batcher.run(open_rx));
handle
}
enum Work {
Open(OpenSlotRequest),
Control(DrainedControl),
Slot(u32, SlotItem),
Linger,
}
struct Batcher {
peer: WorkerId,
metrics: Option<MuxMetricsHandle>,
handle: Arc<BatcherHandle>,
epochs: Arc<AtomicU64>,
batchers: Arc<BatcherMap>,
cancel: CancellationToken,
control: Arc<ControlInbox>,
gate: FlushGate,
writer: BatchWriter,
slots: EgressSlots,
streams: SelectAll<SlotStream>,
stopping: bool,
#[cfg(test)]
hooks: Option<Arc<TestHooks>>,
}
impl Batcher {
async fn run(mut self, opens: flume::Receiver<OpenSlotRequest>) {
let cancel = self.cancel.clone();
let control = Arc::clone(&self.control);
loop {
let deadline = self.gate.deadline();
let work = tokio::select! {
biased;
() = cancel.cancelled() => break,
open = opens.recv_async() => match open {
Ok(open) => Work::Open(open),
Err(_) => break,
},
() = control.wait() => match control.take() {
Some(drained) => Work::Control(drained),
None => continue,
},
Some((index, item)) = self.streams.next() => Work::Slot(index, item),
() = linger_until(deadline) => Work::Linger,
};
self.handle.mark_active();
self.dispatch(work).await;
#[cfg(test)]
if let Some(hooks) = self.hooks.clone() {
hooks.barrier().await;
}
while !self.stopping && self.drain_once(&opens).await {}
let kicked = self.gate.take_kick();
if kicked || self.stopping || self.gate.should_flush() {
self.flush().await;
}
if self.stopping {
return self.teardown(false);
}
}
self.teardown(true);
}
async fn drain_once(&mut self, opens: &flume::Receiver<OpenSlotRequest>) -> bool {
if let Ok(open) = opens.try_recv() {
self.dispatch(Work::Open(open)).await;
return true;
}
if let Some(drained) = self.control.take() {
self.dispatch(Work::Control(drained)).await;
return true;
}
match self.streams.next().now_or_never() {
Some(Some((index, item))) => {
self.dispatch(Work::Slot(index, item)).await;
true
}
_ => false,
}
}
async fn dispatch(&mut self, work: Work) {
match work {
Work::Slot(index, SlotItem::Frame(bytes)) => self.on_frame(index, bytes).await,
Work::Slot(index, SlotItem::InletClosed) => self.on_inlet_closed(index).await,
Work::Open(request) => self.on_open_slot(request).await,
Work::Control(drained) => self.on_control(drained).await,
Work::Linger => {}
}
}
async fn on_control(&mut self, drained: DrainedControl) {
if drained.flush {
self.gate.kick();
}
for (raw, entry) in drained.peers {
self.on_reply(SlotId::from_raw(raw), entry).await;
}
for (raw, entry) in drained.mine {
self.on_owned_control(SlotId::from_raw(raw), entry).await;
}
if drained.retire {
self.on_retire();
}
}
async fn on_owned_control(&mut self, slot: SlotId, entry: OwnedControl) {
if self.slots.get_mut_checked(slot).is_none() {
if entry.singleton == Some(false)
&& let Some(metrics) = &self.metrics
{
metrics.record_dropped(MuxDropReason::StaleSingleton);
}
return;
}
if entry.singleton == Some(false) {
self.epoch_death();
return;
}
if let Some(reason) = entry.close {
self.on_peer_closed(slot, reason);
return;
}
let mut touched = false;
if let Some(live) = self.slots.get_mut_checked(slot) {
if entry.credit > 0 {
live.credit.grant(entry.credit);
touched = true;
}
if entry.singleton == Some(true) {
live.unfence();
touched = true;
}
}
if touched {
self.release_withheld(slot.index()).await;
}
}
async fn on_open_slot(&mut self, request: OpenSlotRequest) {
let OpenSlotRequest {
anchor_id,
session_id,
inlet,
credit,
slot_byte_budget,
ack,
} = request;
if self.handle.is_retired() {
let _ = ack.send(Err(OpenRejected::Retired));
return;
}
let (id, stream) = match self.slots.allocate(inlet, credit, slot_byte_budget) {
Ok(allocated) => allocated,
Err(error) => {
let _ = ack.send(Err(error.into()));
return;
}
};
self.streams.push(stream);
self.publish_live_slots();
if let Some(metrics) = &self.metrics {
metrics.slot_opened();
}
let seq = self
.slots
.get_mut(id.index())
.map_or(0, |entry| entry.take_seq());
self.ensure_batch();
if let Some(encoder) = self.writer.encoder() {
let _ = encoder.push_open_slot(id, seq, anchor_id, session_id);
self.gate.stage_urgent(1);
}
self.flush().await;
let _ = ack.send(Ok(()));
}
fn on_peer_closed(&mut self, slot: SlotId, reason: CloseReason) {
if self.slots.get_mut_checked(slot).is_some() {
tracing::debug!(slot = ?slot, ?reason, "messenger mux: peer closed our egress slot");
self.close_local(slot.index());
}
}
async fn on_reply(&mut self, slot: SlotId, entry: PeerControl) {
if entry.credit > 0 {
self.push_reply(|encoder| encoder.push_credit_update(slot, 0, entry.credit))
.await;
}
if let Some(reason) = entry.close {
self.push_reply(|encoder| encoder.push_close_slot(slot, 0, reason))
.await;
}
}
async fn push_reply(
&mut self,
write: impl FnOnce(&mut BatchEncoder) -> Result<(), EncodeError>,
) {
let needed = record_encoded_len(4).unwrap_or(usize::MAX);
self.ensure_batch();
if !self.fits(needed, 1) {
self.flush().await;
self.ensure_batch();
}
if let Some(encoder) = self.writer.encoder() {
let _ = write(encoder);
self.gate.stage_urgent(1);
}
}
fn on_retire(&mut self) {
if self.slots.live() == 0 {
self.stopping = true;
return;
}
match self.batchers.entry(self.peer) {
dashmap::mapref::entry::Entry::Vacant(vacant) => {
self.handle.retired.store(false, Ordering::Release);
self.handle.mark_active();
vacant.insert(Arc::clone(&self.handle));
}
dashmap::mapref::entry::Entry::Occupied(_) => {
self.stopping = true;
}
}
}
fn ensure_batch(&mut self) -> usize {
self.writer.ensure_batch()
}
fn fits(&self, bytes: usize, records: u16) -> bool {
self.writer.fits(bytes, records)
}
async fn flush(&mut self) {
self.gate.cleared();
if let Err(writer::FlushFailed(error)) = self.writer.flush().await {
tracing::warn!(
peer = %self.peer,
epoch = self.writer.epoch(),
%error,
"messenger mux: batch was never admitted; failing the peer epoch"
);
self.epoch_death();
}
}
fn close_local(&mut self, index: u32) {
if self.slots.close(index) {
if let Some(metrics) = &self.metrics {
metrics.slot_closed();
}
self.publish_live_slots();
}
}
fn publish_live_slots(&self) {
self.handle
.live_slots
.store(self.slots.live(), Ordering::Relaxed);
}
fn epoch_death(&mut self) {
let closed = self.slots.close_all();
self.streams = SelectAll::new();
if let Some(metrics) = &self.metrics {
metrics.epoch_death();
for _ in 0..closed {
metrics.slot_closed();
}
}
self.publish_live_slots();
self.gate.discarded();
self.writer
.reset_epoch(self.epochs.fetch_add(1, Ordering::Relaxed));
}
fn teardown(&mut self, unregister: bool) {
self.handle.alive.store(false, Ordering::Release);
self.gate.discarded();
let closed = self.slots.close_all();
self.streams = SelectAll::new();
if let Some(metrics) = &self.metrics {
for _ in 0..closed {
metrics.slot_closed();
}
}
self.publish_live_slots();
if unregister {
let handle = Arc::clone(&self.handle);
self.batchers
.remove_if(&self.peer, |_, entry| Arc::ptr_eq(entry, &handle));
}
}
}