use std::collections::HashMap;
use std::sync::Mutex;
use tokio::sync::Notify;
use super::super::protocol::{CloseReason, SlotId};
use crate::observability::MuxMetricsHandle;
const MAX_PENDING_CONTROL: usize = 4096;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct OwnedControl {
pub(super) credit: u32,
pub(super) close: Option<CloseReason>,
pub(super) singleton: Option<bool>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) struct PeerControl {
pub(super) credit: u32,
pub(super) close: Option<CloseReason>,
}
#[derive(Debug, Default)]
struct ControlState {
pub(super) retire: bool,
pub(super) flush: bool,
mine: HashMap<u32, OwnedControl>,
peers: HashMap<u32, PeerControl>,
refused: u64,
}
impl ControlState {
fn is_idle(&self) -> bool {
!self.retire && !self.flush && self.mine.is_empty() && self.peers.is_empty()
}
fn retire(&mut self) {
self.retire = true;
}
fn kick_flush(&mut self) {
self.flush = true;
}
#[cfg(test)]
fn len(&self) -> usize {
self.mine.len() + self.peers.len()
}
fn drain(&mut self) -> DrainedControl {
DrainedControl {
retire: std::mem::take(&mut self.retire),
flush: std::mem::take(&mut self.flush),
mine: std::mem::take(&mut self.mine),
peers: std::mem::take(&mut self.peers),
}
}
fn entry_mine(&mut self, slot: SlotId) -> Option<&mut OwnedControl> {
Self::slot_entry(&mut self.mine, &mut self.refused, slot)
}
fn entry_peer(&mut self, slot: SlotId) -> Option<&mut PeerControl> {
Self::slot_entry(&mut self.peers, &mut self.refused, slot)
}
fn slot_entry<'a, T: Default>(
map: &'a mut HashMap<u32, T>,
refused: &mut u64,
slot: SlotId,
) -> Option<&'a mut T> {
let key = slot.raw();
if !map.contains_key(&key) && map.len() >= MAX_PENDING_CONTROL {
*refused = refused.saturating_add(1);
return None;
}
Some(map.entry(key).or_default())
}
}
pub(super) struct DrainedControl {
pub(super) retire: bool,
pub(super) flush: bool,
pub(super) mine: HashMap<u32, OwnedControl>,
pub(super) peers: HashMap<u32, PeerControl>,
}
#[derive(Default)]
pub(super) struct ControlInbox {
state: Mutex<ControlState>,
notify: Notify,
metrics: Option<MuxMetricsHandle>,
}
impl ControlInbox {
pub(super) fn new(metrics: Option<MuxMetricsHandle>) -> Self {
Self {
state: Mutex::new(ControlState::default()),
notify: Notify::new(),
metrics,
}
}
pub(super) async fn wait(&self) {
loop {
let notified = self.notify.notified();
if !self.lock().is_idle() {
return;
}
notified.await;
}
}
pub(super) fn take(&self) -> Option<DrainedControl> {
let mut state = self.lock();
if state.is_idle() {
return None;
}
Some(state.drain())
}
#[cfg(test)]
pub(super) fn pending_len(&self) -> usize {
self.lock().len()
}
#[cfg(test)]
pub(super) fn refused(&self) -> u64 {
self.lock().refused
}
pub(super) fn grant(&self, slot: SlotId, delta: u32) {
self.mutate(|state| {
if let Some(entry) = state.entry_mine(slot) {
entry.credit = entry.credit.saturating_add(delta);
}
});
}
pub(super) fn peer_closed(&self, slot: SlotId, reason: CloseReason) {
self.mutate(|state| {
if let Some(entry) = state.entry_mine(slot) {
entry.close.get_or_insert(reason);
}
});
}
pub(super) fn singleton_resolved(&self, slot: SlotId, admitted: bool) {
self.mutate(|state| {
if let Some(entry) = state.entry_mine(slot) {
entry.singleton = Some(entry.singleton.unwrap_or(true) && admitted);
}
});
}
pub(super) fn reply_credit(&self, slot: SlotId, delta: u32) {
self.mutate(|state| {
if let Some(entry) = state.entry_peer(slot) {
entry.credit = entry.credit.saturating_add(delta);
}
});
}
pub(super) fn reply_close(&self, slot: SlotId, reason: CloseReason) {
self.mutate(|state| {
if let Some(entry) = state.entry_peer(slot) {
entry.close.get_or_insert(reason);
}
});
}
pub(super) fn retire(&self) {
self.mutate(ControlState::retire);
}
pub(super) fn kick_flush(&self) {
self.mutate(ControlState::kick_flush);
}
fn mutate(&self, apply: impl FnOnce(&mut ControlState)) {
let refused = {
let mut state = self.lock();
let before = state.refused;
apply(&mut state);
state.refused - before
};
if refused > 0
&& let Some(metrics) = &self.metrics
{
for _ in 0..refused {
metrics.control_refused();
}
}
self.notify.notify_one();
}
fn lock(&self) -> std::sync::MutexGuard<'_, ControlState> {
self.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn slot(index: u32, generation: u8) -> SlotId {
SlotId::new(index, generation).expect("index fits u24")
}
#[test]
fn credit_accumulates_into_one_entry() {
let inbox = ControlInbox::default();
let id = slot(3, 0);
for _ in 0..10_000 {
inbox.grant(id, 1);
}
assert_eq!(inbox.pending_len(), 1, "ten thousand grants, one entry");
let drained = inbox.take().expect("something pending");
assert_eq!(drained.mine[&id.raw()].credit, 10_000);
assert!(inbox.take().is_none(), "the drain leaves nothing behind");
}
#[test]
fn a_close_dominates_and_the_first_reason_wins() {
let inbox = ControlInbox::default();
let id = slot(1, 0);
inbox.grant(id, 5);
inbox.peer_closed(id, CloseReason::UnknownSlot);
inbox.peer_closed(id, CloseReason::ProtocolError);
inbox.grant(id, 5);
let drained = inbox.take().expect("something pending");
let entry = drained.mine[&id.raw()];
assert_eq!(entry.close, Some(CloseReason::UnknownSlot));
assert_eq!(
entry.credit, 10,
"credit still merges; the batcher discards it with the slot"
);
}
#[test]
fn a_failed_singleton_survives_successful_ones() {
let inbox = ControlInbox::default();
let id = slot(2, 7);
inbox.singleton_resolved(id, true);
inbox.singleton_resolved(id, false);
inbox.singleton_resolved(id, true);
let drained = inbox.take().expect("something pending");
assert_eq!(
drained.mine[&id.raw()].singleton,
Some(false),
"a failed admission is epoch death and must not coalesce away"
);
}
#[test]
fn generations_do_not_share_an_entry() {
let inbox = ControlInbox::default();
inbox.grant(slot(4, 0), 1);
inbox.grant(slot(4, 1), 2);
assert_eq!(
inbox.pending_len(),
2,
"a grant for a retired generation must not credit the live one"
);
}
#[test]
fn the_cap_refuses_new_keys_rather_than_growing() {
let inbox = ControlInbox::default();
for index in 0..(MAX_PENDING_CONTROL as u32 + 500) {
inbox.grant(slot(index, 0), 1);
}
assert_eq!(inbox.pending_len(), MAX_PENDING_CONTROL);
assert_eq!(inbox.refused(), 500);
inbox.grant(slot(0, 0), 41);
let drained = inbox.take().expect("something pending");
assert_eq!(drained.mine[&slot(0, 0).raw()].credit, 42);
}
#[test]
fn a_thousand_flush_kicks_are_one_bit() {
let inbox = ControlInbox::default();
for _ in 0..1_000 {
inbox.kick_flush();
}
assert_eq!(
inbox.pending_len(),
0,
"a kick is a flag, so it never grows the slot maps the cap protects"
);
let drained = inbox.take().expect("something pending");
assert!(drained.flush, "the drain carries the kick");
assert!(
inbox.take().is_none(),
"and takes it, so one kick is not served twice"
);
}
#[tokio::test]
async fn a_flush_kick_wakes_a_parked_batcher() {
let inbox = ControlInbox::default();
inbox.kick_flush();
tokio::time::timeout(std::time::Duration::from_secs(5), inbox.wait())
.await
.expect("a kick must wake the batcher like any other control");
}
#[tokio::test]
async fn wait_returns_for_a_change_made_before_it_was_called() {
let inbox = ControlInbox::default();
inbox.retire();
tokio::time::timeout(std::time::Duration::from_secs(5), inbox.wait())
.await
.expect("a permit set before the wait must still wake it");
}
}