use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use super::counts::{ConsoleCounts, ConsoleCountsSnapshot, ConsoleFailure, add, failed, set};
use super::error::ConsoleError;
use super::model::{TopologyChange, TopologyModel};
use super::publisher::ServerPublisher;
use super::records::{canonical_bytes, tap_batch_record};
use super::wall::WallBase;
use crate::tap::error::lock;
use crate::tap::{DEFAULT_TAP_CAPACITY, Tap, TapError, TapFilter, TapSubscription, TapWaker};
pub const TOPOLOGY_SUBJECT: &str = "console.topology";
pub const TAP_SUBJECT_PREFIX: &str = "console.tap.";
pub const MAX_LIVE_TAPS: usize = 256;
#[derive(Debug)]
struct TopologyState {
model: TopologyModel,
stale: bool,
subscribers: usize,
}
#[derive(Debug)]
struct AttachedTap {
subscription: TapSubscription,
subscribers: usize,
pending: VecDeque<Vec<u8>>,
generation: u64,
}
#[derive(Debug, Default)]
struct Taps {
live: BTreeMap<String, AttachedTap>,
reserved: BTreeMap<String, u64>,
retiring: Vec<String>,
generations: u64,
}
type NotifierSlot = Arc<Mutex<Option<TapWaker>>>;
#[cfg(test)]
#[derive(Clone, Copy, Debug)]
pub(crate) enum HubLock {
Topology,
Taps,
Notifier,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct PumpReport {
pub topology_records: u64,
pub tap_records: u64,
pub publish_failures: u64,
pub retired: u64,
}
pub struct ConsoleHub {
device: String,
wall: WallBase,
tap: Tap,
topology: Mutex<TopologyState>,
taps: Mutex<Taps>,
notifier: NotifierSlot,
counts: Arc<ConsoleCounts>,
#[cfg(test)]
sent: Mutex<Vec<(String, Vec<u8>)>>,
#[cfg(test)]
emits: std::sync::atomic::AtomicU64,
#[cfg(test)]
topology_locks: std::sync::atomic::AtomicU64,
#[cfg(test)]
applied: std::sync::Condvar,
#[cfg(test)]
failing_take: Mutex<Option<String>>,
}
impl std::fmt::Debug for ConsoleHub {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ConsoleHub")
.field("device", &self.device)
.field("tap", &self.tap)
.finish_non_exhaustive()
}
}
fn forwarding_waker(slot: NotifierSlot, counts: Arc<ConsoleCounts>) -> TapWaker {
Arc::new(move || match lock(&slot, "console notifier") {
Ok(guard) => {
let notifier = guard.clone();
drop(guard);
if let Some(notifier) = notifier {
notifier();
}
}
Err(_poisoned) => failed(
&counts.console_wake_failures,
1,
ConsoleFailure::ConsoleWake,
),
})
}
#[must_use]
pub fn tappable(subject: &str) -> bool {
!crate::server::connection::is_console_subject(subject)
}
impl ConsoleHub {
pub fn new(device: &str, model: TopologyModel, wall: WallBase) -> Result<Self, ConsoleError> {
Self::with_capacity(device, model, wall, DEFAULT_TAP_CAPACITY)
}
pub fn with_capacity(
device: &str,
model: TopologyModel,
wall: WallBase,
capacity: usize,
) -> Result<Self, ConsoleError> {
if device.trim().is_empty() {
return Err(ConsoleError::EmptyDevice);
}
let notifier = NotifierSlot::default();
let counts = Arc::new(ConsoleCounts::default());
let depot = model.depot();
Ok(Self {
device: device.to_owned(),
wall,
tap: Tap::with_waker(
device,
capacity,
forwarding_waker(Arc::clone(¬ifier), Arc::clone(&counts)),
)
.with_depot(depot),
topology: Mutex::new(TopologyState {
model,
stale: false,
subscribers: 0,
}),
taps: Mutex::new(Taps::default()),
notifier,
counts,
#[cfg(test)]
sent: Mutex::new(Vec::new()),
#[cfg(test)]
emits: std::sync::atomic::AtomicU64::new(0),
#[cfg(test)]
topology_locks: std::sync::atomic::AtomicU64::new(0),
#[cfg(test)]
applied: std::sync::Condvar::new(),
#[cfg(test)]
failing_take: Mutex::new(None),
})
}
#[must_use]
pub fn device(&self) -> &str {
&self.device
}
#[must_use]
pub const fn tap(&self) -> &Tap {
&self.tap
}
#[must_use]
pub fn counts(&self) -> ConsoleCountsSnapshot {
self.counts
.snapshot(self.tap.dropped_total(), self.tap.unsubscribe_failures())
}
fn send(&self, out: &dyn ServerPublisher, subject: &str, bytes: &[u8]) -> bool {
let taken = out.publish_console(subject, bytes).is_ok();
if !taken {
failed(
&self.counts.console_publish_failures,
1,
ConsoleFailure::ConsolePublish,
);
}
#[cfg(test)]
if taken && let Ok(mut sent) = self.sent.lock() {
sent.push((subject.to_owned(), bytes.to_vec()));
}
taken
}
#[cfg(test)]
pub(crate) fn emits(&self) -> u64 {
self.emits.load(std::sync::atomic::Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn participants(&self) -> Vec<Option<String>> {
self.topology
.lock()
.map(|state| state.model.participants())
.unwrap_or_default()
}
#[cfg(test)]
pub(crate) fn topology_locks(&self) -> u64 {
self.topology_locks
.load(std::sync::atomic::Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn wait_for_connections(&self, count: usize) -> bool {
self.topology
.lock()
.ok()
.and_then(|state| {
self.applied
.wait_timeout_while(state, std::time::Duration::from_secs(60), |state| {
state.model.connection_count() != count
})
.ok()
})
.is_some_and(|(_, guard)| !guard.timed_out())
}
#[cfg(test)]
pub(crate) fn fail_next_take(&self, subject: &str) {
if let Ok(mut failing) = self.failing_take.lock() {
*failing = Some(subject.to_owned());
}
}
#[cfg(test)]
fn take_batches(
&self,
subject: &str,
subscription: &TapSubscription,
) -> Result<Vec<crate::tap::TapBatch>, TapError> {
if let Ok(mut failing) = self.failing_take.lock()
&& failing.as_deref() == Some(subject)
{
*failing = None;
return Err(TapError::Poisoned {
what: "tap subscriber",
});
}
subscription.take_batches()
}
fn lock_topology(&self) -> Result<std::sync::MutexGuard<'_, TopologyState>, TapError> {
#[cfg(test)]
self.topology_locks
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
lock(&self.topology, "console topology")
}
pub fn incarnation_started(&self, started: Instant) -> Result<(), TapError> {
let mut state = self.lock_topology()?;
if state.model.incarnation_started(started) {
state.stale = true;
}
drop(state);
Ok(())
}
pub fn suppressed(&self) {
add(&self.counts.tap_suppressed_total, 1);
}
#[cfg(test)]
pub(crate) fn taps_held(&self) -> bool {
self.taps.try_lock().is_err()
}
#[cfg(test)]
pub(crate) fn reserved_taps(&self) -> Option<usize> {
self.taps.lock().ok().map(|taps| taps.reserved.len())
}
#[cfg(test)]
pub(crate) fn sent(&self) -> Option<Vec<(String, Vec<u8>)>> {
self.sent.lock().ok().map(|sent| sent.clone())
}
#[cfg(test)]
pub(crate) fn poison(&self, lock: HubLock) -> bool {
std::thread::scope(|scope| {
scope
.spawn(|| {
let _held = (
matches!(lock, HubLock::Topology).then(|| self.topology.lock()),
matches!(lock, HubLock::Taps).then(|| self.taps.lock()),
matches!(lock, HubLock::Notifier).then(|| self.notifier.lock()),
);
std::panic::resume_unwind(Box::new("poisoned for a test"));
})
.join()
.is_err()
}) && match lock {
HubLock::Topology => self.topology.is_poisoned(),
HubLock::Taps => self.taps.is_poisoned(),
HubLock::Notifier => self.notifier.is_poisoned(),
}
}
pub fn observe_failed(&self) {
failed(
&self.counts.tap_observe_failures,
1,
ConsoleFailure::TapObserve,
);
}
pub fn set_notifier(&self, notifier: TapWaker) -> Result<(), TapError> {
*lock(&self.notifier, "console notifier")? = Some(notifier);
Ok(())
}
fn wake(&self) {
forwarding_waker(Arc::clone(&self.notifier), Arc::clone(&self.counts))();
}
pub fn wake_if_due(&self) -> Result<(), TapError> {
let topology = self.lock_topology()?.subscribers > 0;
let taps = {
let taps = lock(&self.taps, "console taps")?;
!taps.live.is_empty() || !taps.retiring.is_empty()
};
if topology || taps {
self.wake();
}
Ok(())
}
pub fn emit(&self, change: TopologyChange) -> Result<(), TapError> {
#[cfg(test)]
self.emits
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let emit_failed = |_: &TapError| {
failed(
&self.counts.topology_emit_failures,
1,
ConsoleFailure::TopologyEmit,
);
};
let colliding = match &change {
TopologyChange::Worker { identity, .. } => Some(identity),
TopologyChange::Service {
subject,
present: true,
} => Some(subject),
_ => None,
}
.and_then(|id| self.tap.reserved(id).map(|station| (id, station)));
if let Some((id, station)) = colliding {
failed(
&self.counts.station_collisions,
1,
ConsoleFailure::StationCollision,
);
tracing::warn!(id = %id, station, "STATION_COLLISION");
}
match &change {
TopologyChange::Worker {
connection,
identity,
..
} if colliding.is_none() => self.tap.name_station(*connection, identity),
TopologyChange::ConnectionClosed { id } => self
.tap
.forget_station(*id)
.and_then(|()| self.tap.forget_receivers_of(*id)),
_ => Ok(()),
}
.inspect_err(emit_failed)?;
let mut state = self.lock_topology().inspect_err(emit_failed)?;
let changed = state.model.apply(change);
if changed {
state.stale = true;
}
let due = changed && state.subscribers > 0;
drop(state);
#[cfg(test)]
self.applied.notify_all();
if due {
self.wake();
}
Ok(())
}
pub fn tap_room(&self, channel: &str) -> Result<bool, TapError> {
if !channel.starts_with(TAP_SUBJECT_PREFIX) {
return Ok(true);
}
let taps = lock(&self.taps, "console taps")?;
Ok(taps.live.contains_key(channel)
|| taps.reserved.contains_key(channel)
|| taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS)
}
pub fn reserve_tap(&self, channel: &str) -> Result<bool, TapError> {
if !channel.starts_with(TAP_SUBJECT_PREFIX) {
return Ok(true);
}
let mut taps = lock(&self.taps, "console taps")?;
let admitted = if taps.live.contains_key(channel) {
true
} else if let Some(holders) = taps.reserved.get_mut(channel) {
*holders = holders.saturating_add(1);
true
} else if taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS {
taps.reserved.insert(channel.to_owned(), 1);
true
} else {
false
};
drop(taps);
Ok(admitted)
}
pub fn release_tap(
&self,
channel: &str,
retire: impl FnOnce(&str) -> Result<(), ConsoleError>,
) {
if !channel.starts_with(TAP_SUBJECT_PREFIX) {
return;
}
let Ok(mut taps) = lock(&self.taps, "console taps") else {
failed(
&self.counts.tap_detach_failures,
1,
ConsoleFailure::TapDetach,
);
return;
};
let last = match taps.reserved.get_mut(channel) {
Some(holders) if *holders > 1 => {
*holders -= 1;
false
}
Some(_) => {
taps.reserved.remove(channel);
true
}
None => false,
};
if !last || taps.live.contains_key(channel) || retire(channel).is_ok() {
return;
}
taps.retiring.push(channel.to_owned());
drop(taps);
failed(
&self.counts.tap_detach_failures,
1,
ConsoleFailure::TapDetach,
);
self.wake();
}
pub fn attach(&self, channel: &str) -> Result<(), ConsoleError> {
if channel == TOPOLOGY_SUBJECT {
let mut state = self.lock_topology()?;
state.subscribers = state.subscribers.saturating_add(1);
state.stale = true;
drop(state);
self.wake();
return Ok(());
}
let Some(target) = channel.strip_prefix(TAP_SUBJECT_PREFIX) else {
return Ok(());
};
self.attach_tap(channel, target).inspect_err(|_| {
failed(
&self.counts.tap_attach_failures,
1,
ConsoleFailure::TapAttach,
);
})
}
fn attach_tap(&self, channel: &str, target: &str) -> Result<(), ConsoleError> {
let mut taps = lock(&self.taps, "console taps")?;
match taps.reserved.get_mut(channel) {
Some(holders) if *holders > 1 => *holders -= 1,
Some(_) => {
taps.reserved.remove(channel);
}
None => {}
}
if let Some(entry) = taps.live.get_mut(channel) {
entry.subscribers = entry.subscribers.saturating_add(1);
return Ok(());
}
if taps.live.len() >= MAX_LIVE_TAPS {
return Err(ConsoleError::TapLimit {
subject: channel.to_owned(),
limit: MAX_LIVE_TAPS,
});
}
let subscription = self.tap.subscribe(TapFilter {
subject: target.to_owned(),
})?;
taps.retiring.retain(|subject| subject != channel);
let generation = taps.generations;
taps.generations = taps.generations.wrapping_add(1);
taps.live.insert(
channel.to_owned(),
AttachedTap {
subscription,
subscribers: 1,
pending: VecDeque::new(),
generation,
},
);
set(&self.counts.taps_attached, len_u64(taps.live.len()));
drop(taps);
Ok(())
}
pub fn detach(&self, channel: &str) -> Result<(), TapError> {
self.detach_inner(channel).inspect_err(|_| {
failed(
&self.counts.tap_detach_failures,
1,
ConsoleFailure::TapDetach,
);
})
}
fn detach_inner(&self, channel: &str) -> Result<(), TapError> {
if channel == TOPOLOGY_SUBJECT {
let mut state = self.lock_topology()?;
state.subscribers = state.subscribers.saturating_sub(1);
drop(state);
return Ok(());
}
if !channel.starts_with(TAP_SUBJECT_PREFIX) {
return Ok(());
}
let mut taps = lock(&self.taps, "console taps")?;
let last = taps.live.get_mut(channel).is_some_and(|entry| {
entry.subscribers = entry.subscribers.saturating_sub(1);
entry.subscribers == 0
});
if !last {
return Ok(());
}
if let Some(entry) = taps.live.remove(channel) {
add(
&self.counts.tap_records_abandoned,
len_u64(entry.pending.len()),
);
add(
&self.counts.tap_messages_abandoned,
len_u64(entry.subscription.held().unwrap_or_default()),
);
}
taps.retiring.push(channel.to_owned());
set(&self.counts.taps_attached, len_u64(taps.live.len()));
let pending = pending_total(&taps.live);
drop(taps);
set(&self.counts.console_pending_records, pending);
self.wake();
Ok(())
}
pub fn attached_subjects(&self) -> Result<Vec<String>, TapError> {
Ok(lock(&self.taps, "console taps")?
.live
.keys()
.cloned()
.collect())
}
pub fn pump(&self, out: &dyn ServerPublisher) -> Result<PumpReport, ConsoleError> {
add(&self.counts.console_pump_wakes, 1);
let mut report = PumpReport::default();
let topology = self.pump_topology(out, &mut report);
let tapped = self.pump_taps(out, &mut report);
self.retire(out, &mut report)?;
topology.and(tapped).map(|()| report)
}
fn pump_topology(
&self,
out: &dyn ServerPublisher,
report: &mut PumpReport,
) -> Result<(), ConsoleError> {
let mut state = self.lock_topology()?;
if !state.stale || state.subscribers == 0 {
return Ok(());
}
let (document, fault) = state.model.document(&self.wall);
state.stale = false;
drop(state);
if let Some(error) = fault {
failed(
&self.counts.topology_clock_failures,
1,
ConsoleFailure::TopologyClock,
);
tracing::warn!(%error, "CONSOLE_TOPOLOGY_CLOCK");
}
let bytes = canonical_bytes(&document, "topology")?;
if self.send(out, TOPOLOGY_SUBJECT, &bytes) {
add(&self.counts.topology_records_published, 1);
report.topology_records += 1;
} else {
report.publish_failures += 1;
self.lock_topology()?.stale = true;
}
Ok(())
}
fn pump_taps(
&self,
out: &dyn ServerPublisher,
report: &mut PumpReport,
) -> Result<(), ConsoleError> {
let mut due = Vec::new();
let mut first_error = None;
let mut taps = lock(&self.taps, "console taps")?;
for (subject, entry) in &mut taps.live {
if entry.pending.is_empty() {
let target = subject.strip_prefix(TAP_SUBJECT_PREFIX).unwrap_or(subject);
#[cfg(test)]
let taken = self.take_batches(subject, &entry.subscription);
#[cfg(not(test))]
let taken = entry.subscription.take_batches();
match taken {
Ok(batches) => {
for batch in batches {
let encoded =
tap_batch_record(&self.device, target, &batch, &self.wall)
.and_then(|record| canonical_bytes(&record, "tap.batch"));
match encoded {
Ok(bytes) => entry.pending.push_back(bytes),
Err(error) => {
failed(
&self.counts.console_records_lost,
1,
ConsoleFailure::ConsoleRecordsLost,
);
first_error.get_or_insert(error);
}
}
}
}
Err(error) => {
first_error.get_or_insert_with(|| error.into());
}
}
}
if !entry.pending.is_empty() {
due.push((
subject.clone(),
entry.generation,
std::mem::take(&mut entry.pending),
));
}
}
drop(taps);
let mut unsent = Vec::new();
for (subject, generation, mut records) in due {
while let Some(bytes) = records.front() {
if !self.send(out, &subject, bytes) {
report.publish_failures += 1;
unsent.push((subject, generation, records));
break;
}
records.pop_front();
add(&self.counts.tap_records_published, 1);
report.tap_records += 1;
}
}
let mut taps = lock(&self.taps, "console taps")?;
for (subject, generation, records) in unsent {
match taps.live.get_mut(&subject) {
Some(entry) if entry.generation == generation => entry.pending = records,
_ => add(&self.counts.tap_records_abandoned, len_u64(records.len())),
}
}
let pending = pending_total(&taps.live);
drop(taps);
set(&self.counts.console_pending_records, pending);
first_error.map_or(Ok(()), Err)
}
fn retire(
&self,
out: &dyn ServerPublisher,
report: &mut PumpReport,
) -> Result<(), ConsoleError> {
let mut retiring = std::mem::take(&mut lock(&self.taps, "console taps")?.retiring);
while let Some(subject) = retiring.first() {
let mut taps = lock(&self.taps, "console taps")?;
if taps.live.contains_key(subject) || taps.reserved.contains_key(subject) {
drop(taps);
retiring.remove(0);
continue;
}
if let Err(error) = out.retire_console_channel(subject) {
retiring.retain(|subject| !taps.live.contains_key(subject));
retiring.append(&mut taps.retiring);
taps.retiring = retiring;
drop(taps);
failed(
&self.counts.tap_detach_failures,
1,
ConsoleFailure::TapDetach,
);
return Err(error);
}
drop(taps);
retiring.remove(0);
report.retired += 1;
}
Ok(())
}
}
fn len_u64(len: usize) -> u64 {
u64::try_from(len).unwrap_or(u64::MAX)
}
fn pending_total(live: &BTreeMap<String, AttachedTap>) -> u64 {
live.values()
.map(|entry| len_u64(entry.pending.len()))
.sum()
}