1#[path = "../agents/topology_signal.rs"]
20mod agent_signal;
21
22use std::collections::{BTreeMap, VecDeque};
23use std::sync::{Arc, Mutex, OnceLock};
24mod placements;
25mod pump;
26mod taps;
27#[cfg(test)]
28mod test_support;
29mod topology;
30use std::time::Instant;
31
32use super::counts::{ConsoleCounts, ConsoleCountsSnapshot, ConsoleFailure, add, failed, set};
33use super::error::ConsoleError;
34use super::model::{TopologyChange, TopologyModel};
35use super::publisher::ServerPublisher;
36use super::records::{canonical_bytes, tap_batch_record};
37use super::wall::WallBase;
38use crate::tap::error::lock;
39use crate::tap::{DEFAULT_TAP_CAPACITY, Tap, TapError, TapFilter, TapSubscription, TapWaker};
40
41pub const TOPOLOGY_SUBJECT: &str = "console.topology";
43pub const TAP_SUBJECT_PREFIX: &str = "console.tap.";
45pub const MAX_LIVE_TAPS: usize = 256;
47
48#[derive(Debug)]
49struct TopologyState {
50 model: TopologyModel,
51 stale: bool,
52 subscribers: usize,
53 published: Option<(u64, std::collections::BTreeSet<String>)>,
54}
55
56#[derive(Debug)]
57struct AttachedTap {
58 subscription: TapSubscription,
59 subscribers: usize,
60 pending: VecDeque<Vec<u8>>,
61 generation: u64,
64}
65
66#[derive(Debug, Default)]
67struct Taps {
68 live: BTreeMap<String, AttachedTap>,
69 reserved: BTreeMap<String, u64>,
72 retiring: Vec<String>,
73 generations: u64,
75}
76
77type NotifierSlot = Arc<Mutex<Option<TapWaker>>>;
78
79#[cfg(test)]
81#[derive(Clone, Copy, Debug)]
82pub(crate) enum HubLock {
83 Topology,
85 Taps,
87 Notifier,
89}
90
91#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93pub struct PumpReport {
94 pub topology_records: u64,
96 pub tap_records: u64,
98 pub publish_failures: u64,
100 pub retired: u64,
102}
103
104pub struct ConsoleHub {
106 device: String,
107 wall: WallBase,
108 tap: Tap,
109 topology: Mutex<TopologyState>,
110 taps: Mutex<Taps>,
111 notifier: NotifierSlot,
112 counts: Arc<ConsoleCounts>,
113 placements: OnceLock<Arc<super::tap_placement::TapPlacements>>,
114 #[cfg(test)]
116 sent: Mutex<Vec<(String, Vec<u8>)>>,
117 #[cfg(test)]
119 emits: std::sync::atomic::AtomicU64,
120 #[cfg(test)]
122 topology_locks: std::sync::atomic::AtomicU64,
123 #[cfg(test)]
125 applied: std::sync::Condvar,
126 #[cfg(test)]
129 failing_take: Mutex<Option<String>>,
130}
131
132impl std::fmt::Debug for ConsoleHub {
133 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 formatter
135 .debug_struct("ConsoleHub")
136 .field("device", &self.device)
137 .field("tap", &self.tap)
138 .finish_non_exhaustive()
139 }
140}
141
142fn forwarding_waker(slot: NotifierSlot, counts: Arc<ConsoleCounts>) -> TapWaker {
145 Arc::new(move || match lock(&slot, "console notifier") {
146 Ok(guard) => {
147 let notifier = guard.clone();
148 drop(guard);
149 if let Some(notifier) = notifier {
150 notifier();
151 }
152 }
153 Err(_poisoned) => failed(
154 &counts.console_wake_failures,
155 1,
156 ConsoleFailure::ConsoleWake,
157 ),
158 })
159}
160
161#[must_use]
164pub fn tappable(subject: &str) -> bool {
165 !crate::server::connection::is_console_subject(subject)
166}
167
168impl ConsoleHub {
169 pub fn new(device: &str, model: TopologyModel, wall: WallBase) -> Result<Self, ConsoleError> {
175 Self::with_capacity(device, model, wall, DEFAULT_TAP_CAPACITY)
176 }
177
178 #[cfg(any(test, feature = "test-support"))]
181 pub fn set_tap_capacity(&self, capacity: usize) {
182 self.tap.set_capacity(capacity);
183 }
184
185 pub fn with_capacity(
190 device: &str,
191 model: TopologyModel,
192 wall: WallBase,
193 capacity: usize,
194 ) -> Result<Self, ConsoleError> {
195 if device.trim().is_empty() {
196 return Err(ConsoleError::EmptyDevice);
197 }
198 let notifier = NotifierSlot::default();
199 let counts = Arc::new(ConsoleCounts::default());
200 let depot = model.depot();
201 Ok(Self {
202 device: device.to_owned(),
203 wall,
204 tap: Tap::with_waker(
205 device,
206 capacity,
207 forwarding_waker(Arc::clone(¬ifier), Arc::clone(&counts)),
208 )
209 .with_depot(depot),
210 topology: Mutex::new(TopologyState {
211 model,
212 stale: false,
213 subscribers: 0,
214 published: None,
215 }),
216 taps: Mutex::new(Taps::default()),
217 notifier,
218 counts,
219 placements: OnceLock::new(),
220 #[cfg(test)]
221 sent: Mutex::new(Vec::new()),
222 #[cfg(test)]
223 emits: std::sync::atomic::AtomicU64::new(0),
224 #[cfg(test)]
225 topology_locks: std::sync::atomic::AtomicU64::new(0),
226 #[cfg(test)]
227 applied: std::sync::Condvar::new(),
228 #[cfg(test)]
229 failing_take: Mutex::new(None),
230 })
231 }
232
233 #[must_use]
235 pub fn device(&self) -> &str {
236 &self.device
237 }
238
239 #[must_use]
241 pub const fn tap(&self) -> &Tap {
242 &self.tap
243 }
244
245 pub fn serving_station(&self, subject: &str) -> Result<Option<String>, TapError> {
250 Ok(self.lock_topology()?.model.serving_station(subject))
251 }
252
253 #[must_use]
255 pub fn counts(&self) -> ConsoleCountsSnapshot {
256 self.counts
257 .snapshot(self.tap.dropped_total(), self.tap.unsubscribe_failures())
258 }
259
260 fn send(&self, out: &dyn ServerPublisher, subject: &str, bytes: &[u8]) -> bool {
263 let taken = out.publish_console(subject, bytes).is_ok();
264 if !taken {
265 failed(
266 &self.counts.console_publish_failures,
267 1,
268 ConsoleFailure::ConsolePublish,
269 );
270 }
271 #[cfg(test)]
272 if taken && let Ok(mut sent) = self.sent.lock() {
273 sent.push((subject.to_owned(), bytes.to_vec()));
274 }
275 taken
276 }
277
278 fn lock_topology(&self) -> Result<std::sync::MutexGuard<'_, TopologyState>, TapError> {
280 #[cfg(test)]
281 self.topology_locks
282 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
283 lock(&self.topology, "console topology")
284 }
285
286 pub fn incarnation_started(&self, started: Instant) -> Result<(), TapError> {
292 let mut state = self.lock_topology()?;
293 if state.model.incarnation_started(started) {
294 state.stale = true;
295 }
296 drop(state);
297 Ok(())
298 }
299
300 pub fn suppressed(&self) {
302 add(&self.counts.tap_suppressed_total, 1);
303 }
304
305 #[cfg(test)]
307 pub(crate) fn taps_held(&self) -> bool {
308 self.taps.try_lock().is_err()
309 }
310
311 #[cfg(test)]
313 pub(crate) fn reserved_taps(&self) -> Option<usize> {
314 self.taps.lock().ok().map(|taps| taps.reserved.len())
315 }
316
317 #[cfg(test)]
320 pub(crate) fn sent(&self) -> Option<Vec<(String, Vec<u8>)>> {
321 self.sent.lock().ok().map(|sent| sent.clone())
322 }
323
324 #[cfg(test)]
327 pub(crate) fn poison(&self, lock: HubLock) -> bool {
328 std::thread::scope(|scope| {
329 scope
330 .spawn(|| {
331 let _held = (
332 matches!(lock, HubLock::Topology).then(|| self.topology.lock()),
333 matches!(lock, HubLock::Taps).then(|| self.taps.lock()),
334 matches!(lock, HubLock::Notifier).then(|| self.notifier.lock()),
335 );
336 std::panic::resume_unwind(Box::new("poisoned for a test"));
337 })
338 .join()
339 .is_err()
340 }) && match lock {
341 HubLock::Topology => self.topology.is_poisoned(),
342 HubLock::Taps => self.taps.is_poisoned(),
343 HubLock::Notifier => self.notifier.is_poisoned(),
344 }
345 }
346
347 pub fn observe_failed(&self) {
350 failed(
351 &self.counts.tap_observe_failures,
352 1,
353 ConsoleFailure::TapObserve,
354 );
355 }
356
357 pub fn set_notifier(&self, notifier: TapWaker) -> Result<(), TapError> {
363 *lock(&self.notifier, "console notifier")? = Some(notifier);
364 Ok(())
365 }
366
367 fn wake(&self) {
368 forwarding_waker(Arc::clone(&self.notifier), Arc::clone(&self.counts))();
369 }
370
371 pub fn wake_if_due(&self) -> Result<(), TapError> {
379 let topology = self.lock_topology()?.subscribers > 0;
380 let taps = {
381 let taps = lock(&self.taps, "console taps")?;
382 !taps.live.is_empty() || !taps.retiring.is_empty()
383 };
384 if topology || taps {
385 self.wake();
386 }
387 Ok(())
388 }
389
390 pub fn emit(&self, change: TopologyChange) -> Result<(), TapError> {
397 #[cfg(test)]
398 self.emits
399 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
400 let emit_failed = |_: &TapError| {
401 failed(
402 &self.counts.topology_emit_failures,
403 1,
404 ConsoleFailure::TopologyEmit,
405 );
406 };
407 let colliding = match &change {
411 TopologyChange::Worker { identity, .. } => Some(identity),
412 TopologyChange::Service {
413 subject,
414 present: true,
415 } => Some(subject),
416 _ => None,
417 }
418 .and_then(|id| self.tap.reserved(id).map(|station| (id, station)));
419 if let Some((id, station)) = colliding {
420 failed(
421 &self.counts.station_collisions,
422 1,
423 ConsoleFailure::StationCollision,
424 );
425 tracing::warn!(id = %id, station, "STATION_COLLISION");
426 }
427 if colliding.is_none() {
428 self.offer_change(&change);
429 }
430 match &change {
433 TopologyChange::Worker {
434 connection,
435 identity,
436 ..
437 } if colliding.is_none() => self.tap.name_station(*connection, identity),
438 TopologyChange::ConnectionClosed { id } => self
439 .tap
440 .forget_station(*id)
441 .and_then(|()| self.tap.forget_receivers_of(*id)),
442 _ => Ok(()),
443 }
444 .inspect_err(emit_failed)?;
445 let mut state = self.lock_topology().inspect_err(emit_failed)?;
446 let changed = state.model.apply(change);
447 if changed {
448 state.stale = true;
449 }
450 let due = changed && state.subscribers > 0;
451 drop(state);
452 #[cfg(test)]
453 self.applied.notify_all();
454 if due {
455 self.wake();
456 }
457 Ok(())
458 }
459
460 pub fn pump(&self, out: &dyn ServerPublisher) -> Result<PumpReport, ConsoleError> {
471 add(&self.counts.console_pump_wakes, 1);
472 let mut report = PumpReport::default();
473 let topology = self.pump_topology(out, &mut report);
476 let tapped = self.pump_taps(out, &mut report);
477 self.retire(out, &mut report)?;
478 topology.and(tapped).map(|()| report)
479 }
480}
481
482fn len_u64(len: usize) -> u64 {
483 u64::try_from(len).unwrap_or(u64::MAX)
484}
485
486fn pending_total(live: &BTreeMap<String, AttachedTap>) -> u64 {
487 live.values()
488 .map(|entry| len_u64(entry.pending.len()))
489 .sum()
490}