1use std::collections::{BTreeMap, VecDeque};
20use std::sync::{Arc, Mutex};
21use std::time::Instant;
22
23use super::counts::{ConsoleCounts, ConsoleCountsSnapshot, ConsoleFailure, add, failed, set};
24use super::error::ConsoleError;
25use super::model::{TopologyChange, TopologyModel};
26use super::publisher::ServerPublisher;
27use super::records::{canonical_bytes, tap_batch_record};
28use super::wall::WallBase;
29use crate::tap::error::lock;
30use crate::tap::{DEFAULT_TAP_CAPACITY, Tap, TapError, TapFilter, TapSubscription, TapWaker};
31
32pub const TOPOLOGY_SUBJECT: &str = "console.topology";
34pub const TAP_SUBJECT_PREFIX: &str = "console.tap.";
36pub const MAX_LIVE_TAPS: usize = 256;
38
39#[derive(Debug)]
40struct TopologyState {
41 model: TopologyModel,
42 stale: bool,
43 subscribers: usize,
44}
45
46#[derive(Debug)]
47struct AttachedTap {
48 subscription: TapSubscription,
49 subscribers: usize,
50 pending: VecDeque<Vec<u8>>,
51 generation: u64,
54}
55
56#[derive(Debug, Default)]
57struct Taps {
58 live: BTreeMap<String, AttachedTap>,
59 reserved: BTreeMap<String, u64>,
62 retiring: Vec<String>,
63 generations: u64,
65}
66
67type NotifierSlot = Arc<Mutex<Option<TapWaker>>>;
68
69#[cfg(test)]
71#[derive(Clone, Copy, Debug)]
72pub(crate) enum HubLock {
73 Topology,
75 Taps,
77 Notifier,
79}
80
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub struct PumpReport {
84 pub topology_records: u64,
86 pub tap_records: u64,
88 pub publish_failures: u64,
90 pub retired: u64,
92}
93
94pub struct ConsoleHub {
96 device: String,
97 wall: WallBase,
98 tap: Tap,
99 topology: Mutex<TopologyState>,
100 taps: Mutex<Taps>,
101 notifier: NotifierSlot,
102 counts: Arc<ConsoleCounts>,
103 #[cfg(test)]
105 sent: Mutex<Vec<(String, Vec<u8>)>>,
106 #[cfg(test)]
108 emits: std::sync::atomic::AtomicU64,
109 #[cfg(test)]
111 topology_locks: std::sync::atomic::AtomicU64,
112 #[cfg(test)]
114 applied: std::sync::Condvar,
115 #[cfg(test)]
118 failing_take: Mutex<Option<String>>,
119}
120
121impl std::fmt::Debug for ConsoleHub {
122 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 formatter
124 .debug_struct("ConsoleHub")
125 .field("device", &self.device)
126 .field("tap", &self.tap)
127 .finish_non_exhaustive()
128 }
129}
130
131fn forwarding_waker(slot: NotifierSlot, counts: Arc<ConsoleCounts>) -> TapWaker {
134 Arc::new(move || match lock(&slot, "console notifier") {
135 Ok(guard) => {
136 let notifier = guard.clone();
137 drop(guard);
138 if let Some(notifier) = notifier {
139 notifier();
140 }
141 }
142 Err(_poisoned) => failed(
143 &counts.console_wake_failures,
144 1,
145 ConsoleFailure::ConsoleWake,
146 ),
147 })
148}
149
150#[must_use]
153pub fn tappable(subject: &str) -> bool {
154 !crate::server::connection::is_console_subject(subject)
155}
156
157impl ConsoleHub {
158 pub fn new(device: &str, model: TopologyModel, wall: WallBase) -> Result<Self, ConsoleError> {
164 Self::with_capacity(device, model, wall, DEFAULT_TAP_CAPACITY)
165 }
166
167 pub fn with_capacity(
172 device: &str,
173 model: TopologyModel,
174 wall: WallBase,
175 capacity: usize,
176 ) -> Result<Self, ConsoleError> {
177 if device.trim().is_empty() {
178 return Err(ConsoleError::EmptyDevice);
179 }
180 let notifier = NotifierSlot::default();
181 let counts = Arc::new(ConsoleCounts::default());
182 let depot = model.depot();
183 Ok(Self {
184 device: device.to_owned(),
185 wall,
186 tap: Tap::with_waker(
187 device,
188 capacity,
189 forwarding_waker(Arc::clone(¬ifier), Arc::clone(&counts)),
190 )
191 .with_depot(depot),
192 topology: Mutex::new(TopologyState {
193 model,
194 stale: false,
195 subscribers: 0,
196 }),
197 taps: Mutex::new(Taps::default()),
198 notifier,
199 counts,
200 #[cfg(test)]
201 sent: Mutex::new(Vec::new()),
202 #[cfg(test)]
203 emits: std::sync::atomic::AtomicU64::new(0),
204 #[cfg(test)]
205 topology_locks: std::sync::atomic::AtomicU64::new(0),
206 #[cfg(test)]
207 applied: std::sync::Condvar::new(),
208 #[cfg(test)]
209 failing_take: Mutex::new(None),
210 })
211 }
212
213 #[must_use]
215 pub fn device(&self) -> &str {
216 &self.device
217 }
218
219 #[must_use]
221 pub const fn tap(&self) -> &Tap {
222 &self.tap
223 }
224
225 #[must_use]
227 pub fn counts(&self) -> ConsoleCountsSnapshot {
228 self.counts
229 .snapshot(self.tap.dropped_total(), self.tap.unsubscribe_failures())
230 }
231
232 fn send(&self, out: &dyn ServerPublisher, subject: &str, bytes: &[u8]) -> bool {
235 let taken = out.publish_console(subject, bytes).is_ok();
236 if !taken {
237 failed(
238 &self.counts.console_publish_failures,
239 1,
240 ConsoleFailure::ConsolePublish,
241 );
242 }
243 #[cfg(test)]
244 if taken && let Ok(mut sent) = self.sent.lock() {
245 sent.push((subject.to_owned(), bytes.to_vec()));
246 }
247 taken
248 }
249
250 #[cfg(test)]
252 pub(crate) fn emits(&self) -> u64 {
253 self.emits.load(std::sync::atomic::Ordering::Relaxed)
254 }
255
256 #[cfg(test)]
258 pub(crate) fn participants(&self) -> Vec<Option<String>> {
259 self.topology
260 .lock()
261 .map(|state| state.model.participants())
262 .unwrap_or_default()
263 }
264
265 #[cfg(test)]
267 pub(crate) fn topology_locks(&self) -> u64 {
268 self.topology_locks
269 .load(std::sync::atomic::Ordering::Relaxed)
270 }
271
272 #[cfg(test)]
275 pub(crate) fn wait_for_connections(&self, count: usize) -> bool {
276 self.topology
277 .lock()
278 .ok()
279 .and_then(|state| {
280 self.applied
281 .wait_timeout_while(state, std::time::Duration::from_secs(60), |state| {
282 state.model.connection_count() != count
283 })
284 .ok()
285 })
286 .is_some_and(|(_, guard)| !guard.timed_out())
287 }
288
289 #[cfg(test)]
291 pub(crate) fn fail_next_take(&self, subject: &str) {
292 if let Ok(mut failing) = self.failing_take.lock() {
293 *failing = Some(subject.to_owned());
294 }
295 }
296
297 #[cfg(test)]
300 fn take_batches(
301 &self,
302 subject: &str,
303 subscription: &TapSubscription,
304 ) -> Result<Vec<crate::tap::TapBatch>, TapError> {
305 if let Ok(mut failing) = self.failing_take.lock()
306 && failing.as_deref() == Some(subject)
307 {
308 *failing = None;
309 return Err(TapError::Poisoned {
310 what: "tap subscriber",
311 });
312 }
313 subscription.take_batches()
314 }
315
316 fn lock_topology(&self) -> Result<std::sync::MutexGuard<'_, TopologyState>, TapError> {
318 #[cfg(test)]
319 self.topology_locks
320 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
321 lock(&self.topology, "console topology")
322 }
323
324 pub fn incarnation_started(&self, started: Instant) -> Result<(), TapError> {
330 let mut state = self.lock_topology()?;
331 if state.model.incarnation_started(started) {
332 state.stale = true;
333 }
334 drop(state);
335 Ok(())
336 }
337
338 pub fn suppressed(&self) {
340 add(&self.counts.tap_suppressed_total, 1);
341 }
342
343 #[cfg(test)]
345 pub(crate) fn taps_held(&self) -> bool {
346 self.taps.try_lock().is_err()
347 }
348
349 #[cfg(test)]
351 pub(crate) fn reserved_taps(&self) -> Option<usize> {
352 self.taps.lock().ok().map(|taps| taps.reserved.len())
353 }
354
355 #[cfg(test)]
358 pub(crate) fn sent(&self) -> Option<Vec<(String, Vec<u8>)>> {
359 self.sent.lock().ok().map(|sent| sent.clone())
360 }
361
362 #[cfg(test)]
365 pub(crate) fn poison(&self, lock: HubLock) -> bool {
366 std::thread::scope(|scope| {
367 scope
368 .spawn(|| {
369 let _held = (
370 matches!(lock, HubLock::Topology).then(|| self.topology.lock()),
371 matches!(lock, HubLock::Taps).then(|| self.taps.lock()),
372 matches!(lock, HubLock::Notifier).then(|| self.notifier.lock()),
373 );
374 std::panic::resume_unwind(Box::new("poisoned for a test"));
375 })
376 .join()
377 .is_err()
378 }) && match lock {
379 HubLock::Topology => self.topology.is_poisoned(),
380 HubLock::Taps => self.taps.is_poisoned(),
381 HubLock::Notifier => self.notifier.is_poisoned(),
382 }
383 }
384
385 pub fn observe_failed(&self) {
388 failed(
389 &self.counts.tap_observe_failures,
390 1,
391 ConsoleFailure::TapObserve,
392 );
393 }
394
395 pub fn set_notifier(&self, notifier: TapWaker) -> Result<(), TapError> {
401 *lock(&self.notifier, "console notifier")? = Some(notifier);
402 Ok(())
403 }
404
405 fn wake(&self) {
406 forwarding_waker(Arc::clone(&self.notifier), Arc::clone(&self.counts))();
407 }
408
409 pub fn wake_if_due(&self) -> Result<(), TapError> {
417 let topology = self.lock_topology()?.subscribers > 0;
418 let taps = {
419 let taps = lock(&self.taps, "console taps")?;
420 !taps.live.is_empty() || !taps.retiring.is_empty()
421 };
422 if topology || taps {
423 self.wake();
424 }
425 Ok(())
426 }
427
428 pub fn emit(&self, change: TopologyChange) -> Result<(), TapError> {
435 #[cfg(test)]
436 self.emits
437 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
438 let emit_failed = |_: &TapError| {
439 failed(
440 &self.counts.topology_emit_failures,
441 1,
442 ConsoleFailure::TopologyEmit,
443 );
444 };
445 let colliding = match &change {
449 TopologyChange::Worker { identity, .. } => Some(identity),
450 TopologyChange::Service {
451 subject,
452 present: true,
453 } => Some(subject),
454 _ => None,
455 }
456 .and_then(|id| self.tap.reserved(id).map(|station| (id, station)));
457 if let Some((id, station)) = colliding {
458 failed(
459 &self.counts.station_collisions,
460 1,
461 ConsoleFailure::StationCollision,
462 );
463 tracing::warn!(id = %id, station, "STATION_COLLISION");
464 }
465 match &change {
468 TopologyChange::Worker {
469 connection,
470 identity,
471 } if colliding.is_none() => self.tap.name_station(*connection, identity),
472 TopologyChange::ConnectionClosed { id } => self
473 .tap
474 .forget_station(*id)
475 .and_then(|()| self.tap.forget_receivers_of(*id)),
476 _ => Ok(()),
477 }
478 .inspect_err(emit_failed)?;
479 let mut state = self.lock_topology().inspect_err(emit_failed)?;
480 let changed = state.model.apply(change);
481 if changed {
482 state.stale = true;
483 }
484 let due = changed && state.subscribers > 0;
485 drop(state);
486 #[cfg(test)]
487 self.applied.notify_all();
488 if due {
489 self.wake();
490 }
491 Ok(())
492 }
493
494 pub fn tap_room(&self, channel: &str) -> Result<bool, TapError> {
500 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
501 return Ok(true);
502 }
503 let taps = lock(&self.taps, "console taps")?;
504 Ok(taps.live.contains_key(channel)
505 || taps.reserved.contains_key(channel)
506 || taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS)
507 }
508
509 pub fn reserve_tap(&self, channel: &str) -> Result<bool, TapError> {
518 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
519 return Ok(true);
520 }
521 let mut taps = lock(&self.taps, "console taps")?;
522 let admitted = if taps.live.contains_key(channel) {
523 true
524 } else if let Some(holders) = taps.reserved.get_mut(channel) {
525 *holders = holders.saturating_add(1);
526 true
527 } else if taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS {
528 taps.reserved.insert(channel.to_owned(), 1);
529 true
530 } else {
531 false
532 };
533 drop(taps);
534 Ok(admitted)
535 }
536
537 pub fn release_tap(
545 &self,
546 channel: &str,
547 retire: impl FnOnce(&str) -> Result<(), ConsoleError>,
548 ) {
549 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
550 return;
551 }
552 let Ok(mut taps) = lock(&self.taps, "console taps") else {
553 failed(
554 &self.counts.tap_detach_failures,
555 1,
556 ConsoleFailure::TapDetach,
557 );
558 return;
559 };
560 let last = match taps.reserved.get_mut(channel) {
561 Some(holders) if *holders > 1 => {
562 *holders -= 1;
563 false
564 }
565 Some(_) => {
566 taps.reserved.remove(channel);
567 true
568 }
569 None => false,
570 };
571 if !last || taps.live.contains_key(channel) || retire(channel).is_ok() {
572 return;
573 }
574 taps.retiring.push(channel.to_owned());
575 drop(taps);
576 failed(
577 &self.counts.tap_detach_failures,
578 1,
579 ConsoleFailure::TapDetach,
580 );
581 self.wake();
582 }
583
584 pub fn attach(&self, channel: &str) -> Result<(), ConsoleError> {
594 if channel == TOPOLOGY_SUBJECT {
595 let mut state = self.lock_topology()?;
596 state.subscribers = state.subscribers.saturating_add(1);
597 state.stale = true;
598 drop(state);
599 self.wake();
600 return Ok(());
601 }
602 let Some(target) = channel.strip_prefix(TAP_SUBJECT_PREFIX) else {
603 return Ok(());
604 };
605 self.attach_tap(channel, target).inspect_err(|_| {
606 failed(
607 &self.counts.tap_attach_failures,
608 1,
609 ConsoleFailure::TapAttach,
610 );
611 })
612 }
613
614 fn attach_tap(&self, channel: &str, target: &str) -> Result<(), ConsoleError> {
615 let mut taps = lock(&self.taps, "console taps")?;
616 match taps.reserved.get_mut(channel) {
617 Some(holders) if *holders > 1 => *holders -= 1,
618 Some(_) => {
619 taps.reserved.remove(channel);
620 }
621 None => {}
622 }
623 if let Some(entry) = taps.live.get_mut(channel) {
624 entry.subscribers = entry.subscribers.saturating_add(1);
625 return Ok(());
626 }
627 if taps.live.len() >= MAX_LIVE_TAPS {
628 return Err(ConsoleError::TapLimit {
629 subject: channel.to_owned(),
630 limit: MAX_LIVE_TAPS,
631 });
632 }
633 let subscription = self.tap.subscribe(TapFilter {
634 subject: target.to_owned(),
635 })?;
636 taps.retiring.retain(|subject| subject != channel);
637 let generation = taps.generations;
638 taps.generations = taps.generations.wrapping_add(1);
639 taps.live.insert(
640 channel.to_owned(),
641 AttachedTap {
642 subscription,
643 subscribers: 1,
644 pending: VecDeque::new(),
645 generation,
646 },
647 );
648 set(&self.counts.taps_attached, len_u64(taps.live.len()));
649 drop(taps);
650 Ok(())
651 }
652
653 pub fn detach(&self, channel: &str) -> Result<(), TapError> {
662 self.detach_inner(channel).inspect_err(|_| {
663 failed(
664 &self.counts.tap_detach_failures,
665 1,
666 ConsoleFailure::TapDetach,
667 );
668 })
669 }
670
671 fn detach_inner(&self, channel: &str) -> Result<(), TapError> {
672 if channel == TOPOLOGY_SUBJECT {
673 let mut state = self.lock_topology()?;
674 state.subscribers = state.subscribers.saturating_sub(1);
675 drop(state);
676 return Ok(());
677 }
678 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
679 return Ok(());
680 }
681 let mut taps = lock(&self.taps, "console taps")?;
682 let last = taps.live.get_mut(channel).is_some_and(|entry| {
683 entry.subscribers = entry.subscribers.saturating_sub(1);
684 entry.subscribers == 0
685 });
686 if !last {
687 return Ok(());
688 }
689 if let Some(entry) = taps.live.remove(channel) {
692 add(
693 &self.counts.tap_records_abandoned,
694 len_u64(entry.pending.len()),
695 );
696 add(
697 &self.counts.tap_messages_abandoned,
698 len_u64(entry.subscription.held().unwrap_or_default()),
699 );
700 }
701 taps.retiring.push(channel.to_owned());
702 set(&self.counts.taps_attached, len_u64(taps.live.len()));
703 let pending = pending_total(&taps.live);
704 drop(taps);
705 set(&self.counts.console_pending_records, pending);
706 self.wake();
707 Ok(())
708 }
709
710 pub fn attached_subjects(&self) -> Result<Vec<String>, TapError> {
715 Ok(lock(&self.taps, "console taps")?
716 .live
717 .keys()
718 .cloned()
719 .collect())
720 }
721
722 pub fn pump(&self, out: &dyn ServerPublisher) -> Result<PumpReport, ConsoleError> {
733 add(&self.counts.console_pump_wakes, 1);
734 let mut report = PumpReport::default();
735 let topology = self.pump_topology(out, &mut report);
738 let tapped = self.pump_taps(out, &mut report);
739 self.retire(out, &mut report)?;
740 topology.and(tapped).map(|()| report)
741 }
742
743 fn pump_topology(
744 &self,
745 out: &dyn ServerPublisher,
746 report: &mut PumpReport,
747 ) -> Result<(), ConsoleError> {
748 let mut state = self.lock_topology()?;
749 if !state.stale || state.subscribers == 0 {
750 return Ok(());
751 }
752 let (document, fault) = state.model.document(&self.wall);
753 state.stale = false;
754 drop(state);
755 if let Some(error) = fault {
756 failed(
758 &self.counts.topology_clock_failures,
759 1,
760 ConsoleFailure::TopologyClock,
761 );
762 tracing::warn!(%error, "CONSOLE_TOPOLOGY_CLOCK");
763 }
764 let bytes = canonical_bytes(&document, "topology")?;
765 if self.send(out, TOPOLOGY_SUBJECT, &bytes) {
766 add(&self.counts.topology_records_published, 1);
767 report.topology_records += 1;
768 } else {
769 report.publish_failures += 1;
770 self.lock_topology()?.stale = true;
772 }
773 Ok(())
774 }
775
776 fn pump_taps(
777 &self,
778 out: &dyn ServerPublisher,
779 report: &mut PumpReport,
780 ) -> Result<(), ConsoleError> {
781 let mut due = Vec::new();
782 let mut first_error = None;
785 let mut taps = lock(&self.taps, "console taps")?;
786 for (subject, entry) in &mut taps.live {
787 if entry.pending.is_empty() {
788 let target = subject.strip_prefix(TAP_SUBJECT_PREFIX).unwrap_or(subject);
789 #[cfg(test)]
790 let taken = self.take_batches(subject, &entry.subscription);
791 #[cfg(not(test))]
792 let taken = entry.subscription.take_batches();
793 match taken {
794 Ok(batches) => {
795 for batch in batches {
796 let encoded =
797 tap_batch_record(&self.device, target, &batch, &self.wall)
798 .and_then(|record| canonical_bytes(&record, "tap.batch"));
799 match encoded {
800 Ok(bytes) => entry.pending.push_back(bytes),
801 Err(error) => {
802 failed(
805 &self.counts.console_records_lost,
806 1,
807 ConsoleFailure::ConsoleRecordsLost,
808 );
809 first_error.get_or_insert(error);
810 }
811 }
812 }
813 }
814 Err(error) => {
815 first_error.get_or_insert_with(|| error.into());
816 }
817 }
818 }
819 if !entry.pending.is_empty() {
820 due.push((
821 subject.clone(),
822 entry.generation,
823 std::mem::take(&mut entry.pending),
824 ));
825 }
826 }
827 drop(taps);
828
829 let mut unsent = Vec::new();
830 for (subject, generation, mut records) in due {
831 while let Some(bytes) = records.front() {
832 if !self.send(out, &subject, bytes) {
833 report.publish_failures += 1;
834 unsent.push((subject, generation, records));
835 break;
836 }
837 records.pop_front();
838 add(&self.counts.tap_records_published, 1);
839 report.tap_records += 1;
840 }
841 }
842
843 let mut taps = lock(&self.taps, "console taps")?;
844 for (subject, generation, records) in unsent {
845 match taps.live.get_mut(&subject) {
848 Some(entry) if entry.generation == generation => entry.pending = records,
849 _ => add(&self.counts.tap_records_abandoned, len_u64(records.len())),
850 }
851 }
852 let pending = pending_total(&taps.live);
853 drop(taps);
854 set(&self.counts.console_pending_records, pending);
855 first_error.map_or(Ok(()), Err)
856 }
857
858 fn retire(
859 &self,
860 out: &dyn ServerPublisher,
861 report: &mut PumpReport,
862 ) -> Result<(), ConsoleError> {
863 let mut retiring = std::mem::take(&mut lock(&self.taps, "console taps")?.retiring);
864 while let Some(subject) = retiring.first() {
865 let mut taps = lock(&self.taps, "console taps")?;
869 if taps.live.contains_key(subject) || taps.reserved.contains_key(subject) {
871 drop(taps);
872 retiring.remove(0);
873 continue;
874 }
875 if let Err(error) = out.retire_console_channel(subject) {
876 retiring.retain(|subject| !taps.live.contains_key(subject));
878 retiring.append(&mut taps.retiring);
879 taps.retiring = retiring;
880 drop(taps);
881 failed(
882 &self.counts.tap_detach_failures,
883 1,
884 ConsoleFailure::TapDetach,
885 );
886 return Err(error);
887 }
888 drop(taps);
889 retiring.remove(0);
890 report.retired += 1;
891 }
892 Ok(())
893 }
894}
895
896fn len_u64(len: usize) -> u64 {
897 u64::try_from(len).unwrap_or(u64::MAX)
898}
899
900fn pending_total(live: &BTreeMap<String, AttachedTap>) -> u64 {
901 live.values()
902 .map(|entry| len_u64(entry.pending.len()))
903 .sum()
904}