1use std::collections::{BTreeMap, VecDeque};
20use std::sync::{Arc, Mutex, OnceLock};
21mod placements;
22mod pump;
23mod taps;
24#[cfg(test)]
25mod test_support;
26mod topology;
27use std::time::Instant;
28
29use super::counts::{ConsoleCounts, ConsoleCountsSnapshot, ConsoleFailure, add, failed, set};
30use super::error::ConsoleError;
31use super::model::{TopologyChange, TopologyModel};
32use super::publisher::ServerPublisher;
33use super::records::{canonical_bytes, tap_batch_record};
34use super::wall::WallBase;
35use crate::tap::error::lock;
36use crate::tap::{DEFAULT_TAP_CAPACITY, Tap, TapError, TapFilter, TapSubscription, TapWaker};
37
38pub const TOPOLOGY_SUBJECT: &str = "console.topology";
40pub const TAP_SUBJECT_PREFIX: &str = "console.tap.";
42pub const MAX_LIVE_TAPS: usize = 256;
44
45#[derive(Debug)]
46struct TopologyState {
47 model: TopologyModel,
48 stale: bool,
49 subscribers: usize,
50 published: Option<(u64, std::collections::BTreeSet<String>)>,
51}
52
53#[derive(Debug)]
54struct AttachedTap {
55 subscription: TapSubscription,
56 subscribers: usize,
57 pending: VecDeque<Vec<u8>>,
58 generation: u64,
61}
62
63#[derive(Debug, Default)]
64struct Taps {
65 live: BTreeMap<String, AttachedTap>,
66 reserved: BTreeMap<String, u64>,
69 retiring: Vec<String>,
70 generations: u64,
72}
73
74type NotifierSlot = Arc<Mutex<Option<TapWaker>>>;
75
76#[cfg(test)]
78#[derive(Clone, Copy, Debug)]
79pub(crate) enum HubLock {
80 Topology,
82 Taps,
84 Notifier,
86}
87
88#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
90pub struct PumpReport {
91 pub topology_records: u64,
93 pub tap_records: u64,
95 pub publish_failures: u64,
97 pub retired: u64,
99}
100
101pub struct ConsoleHub {
103 device: String,
104 wall: WallBase,
105 tap: Tap,
106 topology: Mutex<TopologyState>,
107 taps: Mutex<Taps>,
108 notifier: NotifierSlot,
109 counts: Arc<ConsoleCounts>,
110 placements: OnceLock<Arc<super::tap_placement::TapPlacements>>,
111 #[cfg(test)]
113 sent: Mutex<Vec<(String, Vec<u8>)>>,
114 #[cfg(test)]
116 emits: std::sync::atomic::AtomicU64,
117 #[cfg(test)]
119 topology_locks: std::sync::atomic::AtomicU64,
120 #[cfg(test)]
122 applied: std::sync::Condvar,
123 #[cfg(test)]
126 failing_take: Mutex<Option<String>>,
127}
128
129impl std::fmt::Debug for ConsoleHub {
130 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 formatter
132 .debug_struct("ConsoleHub")
133 .field("device", &self.device)
134 .field("tap", &self.tap)
135 .finish_non_exhaustive()
136 }
137}
138
139fn forwarding_waker(slot: NotifierSlot, counts: Arc<ConsoleCounts>) -> TapWaker {
142 Arc::new(move || match lock(&slot, "console notifier") {
143 Ok(guard) => {
144 let notifier = guard.clone();
145 drop(guard);
146 if let Some(notifier) = notifier {
147 notifier();
148 }
149 }
150 Err(_poisoned) => failed(
151 &counts.console_wake_failures,
152 1,
153 ConsoleFailure::ConsoleWake,
154 ),
155 })
156}
157
158#[must_use]
161pub fn tappable(subject: &str) -> bool {
162 !crate::server::connection::is_console_subject(subject)
163}
164
165impl ConsoleHub {
166 pub fn new(device: &str, model: TopologyModel, wall: WallBase) -> Result<Self, ConsoleError> {
172 Self::with_capacity(device, model, wall, DEFAULT_TAP_CAPACITY)
173 }
174
175 pub fn with_capacity(
180 device: &str,
181 model: TopologyModel,
182 wall: WallBase,
183 capacity: usize,
184 ) -> Result<Self, ConsoleError> {
185 if device.trim().is_empty() {
186 return Err(ConsoleError::EmptyDevice);
187 }
188 let notifier = NotifierSlot::default();
189 let counts = Arc::new(ConsoleCounts::default());
190 let depot = model.depot();
191 Ok(Self {
192 device: device.to_owned(),
193 wall,
194 tap: Tap::with_waker(
195 device,
196 capacity,
197 forwarding_waker(Arc::clone(¬ifier), Arc::clone(&counts)),
198 )
199 .with_depot(depot),
200 topology: Mutex::new(TopologyState {
201 model,
202 stale: false,
203 subscribers: 0,
204 published: None,
205 }),
206 taps: Mutex::new(Taps::default()),
207 notifier,
208 counts,
209 placements: OnceLock::new(),
210 #[cfg(test)]
211 sent: Mutex::new(Vec::new()),
212 #[cfg(test)]
213 emits: std::sync::atomic::AtomicU64::new(0),
214 #[cfg(test)]
215 topology_locks: std::sync::atomic::AtomicU64::new(0),
216 #[cfg(test)]
217 applied: std::sync::Condvar::new(),
218 #[cfg(test)]
219 failing_take: Mutex::new(None),
220 })
221 }
222
223 #[must_use]
225 pub fn device(&self) -> &str {
226 &self.device
227 }
228
229 #[must_use]
231 pub const fn tap(&self) -> &Tap {
232 &self.tap
233 }
234
235 pub fn serving_station(&self, subject: &str) -> Result<Option<String>, TapError> {
240 Ok(self.lock_topology()?.model.serving_station(subject))
241 }
242
243 #[must_use]
245 pub fn counts(&self) -> ConsoleCountsSnapshot {
246 self.counts
247 .snapshot(self.tap.dropped_total(), self.tap.unsubscribe_failures())
248 }
249
250 fn send(&self, out: &dyn ServerPublisher, subject: &str, bytes: &[u8]) -> bool {
253 let taken = out.publish_console(subject, bytes).is_ok();
254 if !taken {
255 failed(
256 &self.counts.console_publish_failures,
257 1,
258 ConsoleFailure::ConsolePublish,
259 );
260 }
261 #[cfg(test)]
262 if taken && let Ok(mut sent) = self.sent.lock() {
263 sent.push((subject.to_owned(), bytes.to_vec()));
264 }
265 taken
266 }
267
268 fn lock_topology(&self) -> Result<std::sync::MutexGuard<'_, TopologyState>, TapError> {
270 #[cfg(test)]
271 self.topology_locks
272 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
273 lock(&self.topology, "console topology")
274 }
275
276 pub fn incarnation_started(&self, started: Instant) -> Result<(), TapError> {
282 let mut state = self.lock_topology()?;
283 if state.model.incarnation_started(started) {
284 state.stale = true;
285 }
286 drop(state);
287 Ok(())
288 }
289
290 pub fn suppressed(&self) {
292 add(&self.counts.tap_suppressed_total, 1);
293 }
294
295 #[cfg(test)]
297 pub(crate) fn taps_held(&self) -> bool {
298 self.taps.try_lock().is_err()
299 }
300
301 #[cfg(test)]
303 pub(crate) fn reserved_taps(&self) -> Option<usize> {
304 self.taps.lock().ok().map(|taps| taps.reserved.len())
305 }
306
307 #[cfg(test)]
310 pub(crate) fn sent(&self) -> Option<Vec<(String, Vec<u8>)>> {
311 self.sent.lock().ok().map(|sent| sent.clone())
312 }
313
314 #[cfg(test)]
317 pub(crate) fn poison(&self, lock: HubLock) -> bool {
318 std::thread::scope(|scope| {
319 scope
320 .spawn(|| {
321 let _held = (
322 matches!(lock, HubLock::Topology).then(|| self.topology.lock()),
323 matches!(lock, HubLock::Taps).then(|| self.taps.lock()),
324 matches!(lock, HubLock::Notifier).then(|| self.notifier.lock()),
325 );
326 std::panic::resume_unwind(Box::new("poisoned for a test"));
327 })
328 .join()
329 .is_err()
330 }) && match lock {
331 HubLock::Topology => self.topology.is_poisoned(),
332 HubLock::Taps => self.taps.is_poisoned(),
333 HubLock::Notifier => self.notifier.is_poisoned(),
334 }
335 }
336
337 pub fn observe_failed(&self) {
340 failed(
341 &self.counts.tap_observe_failures,
342 1,
343 ConsoleFailure::TapObserve,
344 );
345 }
346
347 pub fn set_notifier(&self, notifier: TapWaker) -> Result<(), TapError> {
353 *lock(&self.notifier, "console notifier")? = Some(notifier);
354 Ok(())
355 }
356
357 fn wake(&self) {
358 forwarding_waker(Arc::clone(&self.notifier), Arc::clone(&self.counts))();
359 }
360
361 pub fn wake_if_due(&self) -> Result<(), TapError> {
369 let topology = self.lock_topology()?.subscribers > 0;
370 let taps = {
371 let taps = lock(&self.taps, "console taps")?;
372 !taps.live.is_empty() || !taps.retiring.is_empty()
373 };
374 if topology || taps {
375 self.wake();
376 }
377 Ok(())
378 }
379
380 pub fn emit(&self, change: TopologyChange) -> Result<(), TapError> {
387 #[cfg(test)]
388 self.emits
389 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
390 let emit_failed = |_: &TapError| {
391 failed(
392 &self.counts.topology_emit_failures,
393 1,
394 ConsoleFailure::TopologyEmit,
395 );
396 };
397 let colliding = match &change {
401 TopologyChange::Worker { identity, .. } => Some(identity),
402 TopologyChange::Service {
403 subject,
404 present: true,
405 } => Some(subject),
406 _ => None,
407 }
408 .and_then(|id| self.tap.reserved(id).map(|station| (id, station)));
409 if let Some((id, station)) = colliding {
410 failed(
411 &self.counts.station_collisions,
412 1,
413 ConsoleFailure::StationCollision,
414 );
415 tracing::warn!(id = %id, station, "STATION_COLLISION");
416 }
417 if colliding.is_none() {
418 self.offer_change(&change);
419 }
420 match &change {
423 TopologyChange::Worker {
424 connection,
425 identity,
426 ..
427 } if colliding.is_none() => self.tap.name_station(*connection, identity),
428 TopologyChange::ConnectionClosed { id } => self
429 .tap
430 .forget_station(*id)
431 .and_then(|()| self.tap.forget_receivers_of(*id)),
432 _ => Ok(()),
433 }
434 .inspect_err(emit_failed)?;
435 let mut state = self.lock_topology().inspect_err(emit_failed)?;
436 let changed = state.model.apply(change);
437 if changed {
438 state.stale = true;
439 }
440 let due = changed && state.subscribers > 0;
441 drop(state);
442 #[cfg(test)]
443 self.applied.notify_all();
444 if due {
445 self.wake();
446 }
447 Ok(())
448 }
449
450 pub fn pump(&self, out: &dyn ServerPublisher) -> Result<PumpReport, ConsoleError> {
461 add(&self.counts.console_pump_wakes, 1);
462 let mut report = PumpReport::default();
463 let topology = self.pump_topology(out, &mut report);
466 let tapped = self.pump_taps(out, &mut report);
467 self.retire(out, &mut report)?;
468 topology.and(tapped).map(|()| report)
469 }
470}
471
472fn len_u64(len: usize) -> u64 {
473 u64::try_from(len).unwrap_or(u64::MAX)
474}
475
476fn pending_total(live: &BTreeMap<String, AttachedTap>) -> u64 {
477 live.values()
478 .map(|entry| len_u64(entry.pending.len()))
479 .sum()
480}