#[path = "../agents/topology_signal.rs"]
mod agent_signal;
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, Mutex, OnceLock};
mod placements;
mod pump;
mod taps;
#[cfg(test)]
mod test_support;
mod topology;
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,
published: Option<(u64, std::collections::BTreeSet<String>)>,
}
#[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>,
placements: OnceLock<Arc<super::tap_placement::TapPlacements>>,
#[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)
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_tap_capacity(&self, capacity: usize) {
self.tap.set_capacity(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,
published: None,
}),
taps: Mutex::new(Taps::default()),
notifier,
counts,
placements: OnceLock::new(),
#[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
}
pub fn serving_station(&self, subject: &str) -> Result<Option<String>, TapError> {
Ok(self.lock_topology()?.model.serving_station(subject))
}
#[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
}
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");
}
if colliding.is_none() {
self.offer_change(&change);
}
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 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 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()
}