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 ..
472 } if colliding.is_none() => self.tap.name_station(*connection, identity),
473 TopologyChange::ConnectionClosed { id } => self
474 .tap
475 .forget_station(*id)
476 .and_then(|()| self.tap.forget_receivers_of(*id)),
477 _ => Ok(()),
478 }
479 .inspect_err(emit_failed)?;
480 let mut state = self.lock_topology().inspect_err(emit_failed)?;
481 let changed = state.model.apply(change);
482 if changed {
483 state.stale = true;
484 }
485 let due = changed && state.subscribers > 0;
486 drop(state);
487 #[cfg(test)]
488 self.applied.notify_all();
489 if due {
490 self.wake();
491 }
492 Ok(())
493 }
494
495 pub fn tap_room(&self, channel: &str) -> Result<bool, TapError> {
501 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
502 return Ok(true);
503 }
504 let taps = lock(&self.taps, "console taps")?;
505 Ok(taps.live.contains_key(channel)
506 || taps.reserved.contains_key(channel)
507 || taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS)
508 }
509
510 pub fn reserve_tap(&self, channel: &str) -> Result<bool, TapError> {
519 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
520 return Ok(true);
521 }
522 let mut taps = lock(&self.taps, "console taps")?;
523 let admitted = if taps.live.contains_key(channel) {
524 true
525 } else if let Some(holders) = taps.reserved.get_mut(channel) {
526 *holders = holders.saturating_add(1);
527 true
528 } else if taps.live.len() + taps.reserved.len() < MAX_LIVE_TAPS {
529 taps.reserved.insert(channel.to_owned(), 1);
530 true
531 } else {
532 false
533 };
534 drop(taps);
535 Ok(admitted)
536 }
537
538 pub fn release_tap(
546 &self,
547 channel: &str,
548 retire: impl FnOnce(&str) -> Result<(), ConsoleError>,
549 ) {
550 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
551 return;
552 }
553 let Ok(mut taps) = lock(&self.taps, "console taps") else {
554 failed(
555 &self.counts.tap_detach_failures,
556 1,
557 ConsoleFailure::TapDetach,
558 );
559 return;
560 };
561 let last = match taps.reserved.get_mut(channel) {
562 Some(holders) if *holders > 1 => {
563 *holders -= 1;
564 false
565 }
566 Some(_) => {
567 taps.reserved.remove(channel);
568 true
569 }
570 None => false,
571 };
572 if !last || taps.live.contains_key(channel) || retire(channel).is_ok() {
573 return;
574 }
575 taps.retiring.push(channel.to_owned());
576 drop(taps);
577 failed(
578 &self.counts.tap_detach_failures,
579 1,
580 ConsoleFailure::TapDetach,
581 );
582 self.wake();
583 }
584
585 pub fn attach(&self, channel: &str) -> Result<(), ConsoleError> {
595 if channel == TOPOLOGY_SUBJECT {
596 let mut state = self.lock_topology()?;
597 state.subscribers = state.subscribers.saturating_add(1);
598 state.stale = true;
599 drop(state);
600 self.wake();
601 return Ok(());
602 }
603 let Some(target) = channel.strip_prefix(TAP_SUBJECT_PREFIX) else {
604 return Ok(());
605 };
606 self.attach_tap(channel, target).inspect_err(|_| {
607 failed(
608 &self.counts.tap_attach_failures,
609 1,
610 ConsoleFailure::TapAttach,
611 );
612 })
613 }
614
615 fn attach_tap(&self, channel: &str, target: &str) -> Result<(), ConsoleError> {
616 let mut taps = lock(&self.taps, "console taps")?;
617 match taps.reserved.get_mut(channel) {
618 Some(holders) if *holders > 1 => *holders -= 1,
619 Some(_) => {
620 taps.reserved.remove(channel);
621 }
622 None => {}
623 }
624 if let Some(entry) = taps.live.get_mut(channel) {
625 entry.subscribers = entry.subscribers.saturating_add(1);
626 return Ok(());
627 }
628 if taps.live.len() >= MAX_LIVE_TAPS {
629 return Err(ConsoleError::TapLimit {
630 subject: channel.to_owned(),
631 limit: MAX_LIVE_TAPS,
632 });
633 }
634 let subscription = self.tap.subscribe(TapFilter {
635 subject: target.to_owned(),
636 })?;
637 taps.retiring.retain(|subject| subject != channel);
638 let generation = taps.generations;
639 taps.generations = taps.generations.wrapping_add(1);
640 taps.live.insert(
641 channel.to_owned(),
642 AttachedTap {
643 subscription,
644 subscribers: 1,
645 pending: VecDeque::new(),
646 generation,
647 },
648 );
649 set(&self.counts.taps_attached, len_u64(taps.live.len()));
650 drop(taps);
651 Ok(())
652 }
653
654 pub fn detach(&self, channel: &str) -> Result<(), TapError> {
663 self.detach_inner(channel).inspect_err(|_| {
664 failed(
665 &self.counts.tap_detach_failures,
666 1,
667 ConsoleFailure::TapDetach,
668 );
669 })
670 }
671
672 fn detach_inner(&self, channel: &str) -> Result<(), TapError> {
673 if channel == TOPOLOGY_SUBJECT {
674 let mut state = self.lock_topology()?;
675 state.subscribers = state.subscribers.saturating_sub(1);
676 drop(state);
677 return Ok(());
678 }
679 if !channel.starts_with(TAP_SUBJECT_PREFIX) {
680 return Ok(());
681 }
682 let mut taps = lock(&self.taps, "console taps")?;
683 let last = taps.live.get_mut(channel).is_some_and(|entry| {
684 entry.subscribers = entry.subscribers.saturating_sub(1);
685 entry.subscribers == 0
686 });
687 if !last {
688 return Ok(());
689 }
690 if let Some(entry) = taps.live.remove(channel) {
693 add(
694 &self.counts.tap_records_abandoned,
695 len_u64(entry.pending.len()),
696 );
697 add(
698 &self.counts.tap_messages_abandoned,
699 len_u64(entry.subscription.held().unwrap_or_default()),
700 );
701 }
702 taps.retiring.push(channel.to_owned());
703 set(&self.counts.taps_attached, len_u64(taps.live.len()));
704 let pending = pending_total(&taps.live);
705 drop(taps);
706 set(&self.counts.console_pending_records, pending);
707 self.wake();
708 Ok(())
709 }
710
711 pub fn attached_subjects(&self) -> Result<Vec<String>, TapError> {
716 Ok(lock(&self.taps, "console taps")?
717 .live
718 .keys()
719 .cloned()
720 .collect())
721 }
722
723 pub fn pump(&self, out: &dyn ServerPublisher) -> Result<PumpReport, ConsoleError> {
734 add(&self.counts.console_pump_wakes, 1);
735 let mut report = PumpReport::default();
736 let topology = self.pump_topology(out, &mut report);
739 let tapped = self.pump_taps(out, &mut report);
740 self.retire(out, &mut report)?;
741 topology.and(tapped).map(|()| report)
742 }
743
744 fn pump_topology(
745 &self,
746 out: &dyn ServerPublisher,
747 report: &mut PumpReport,
748 ) -> Result<(), ConsoleError> {
749 let mut state = self.lock_topology()?;
750 if !state.stale || state.subscribers == 0 {
751 return Ok(());
752 }
753 let (document, fault) = state.model.document(&self.wall);
754 state.stale = false;
755 drop(state);
756 if let Some(error) = fault {
757 failed(
759 &self.counts.topology_clock_failures,
760 1,
761 ConsoleFailure::TopologyClock,
762 );
763 tracing::warn!(%error, "CONSOLE_TOPOLOGY_CLOCK");
764 }
765 let bytes = canonical_bytes(&document, "topology")?;
766 if self.send(out, TOPOLOGY_SUBJECT, &bytes) {
767 add(&self.counts.topology_records_published, 1);
768 report.topology_records += 1;
769 } else {
770 report.publish_failures += 1;
771 self.lock_topology()?.stale = true;
773 }
774 Ok(())
775 }
776
777 fn pump_taps(
778 &self,
779 out: &dyn ServerPublisher,
780 report: &mut PumpReport,
781 ) -> Result<(), ConsoleError> {
782 let mut due = Vec::new();
783 let mut first_error = None;
786 let mut taps = lock(&self.taps, "console taps")?;
787 for (subject, entry) in &mut taps.live {
788 if entry.pending.is_empty() {
789 let target = subject.strip_prefix(TAP_SUBJECT_PREFIX).unwrap_or(subject);
790 #[cfg(test)]
791 let taken = self.take_batches(subject, &entry.subscription);
792 #[cfg(not(test))]
793 let taken = entry.subscription.take_batches();
794 match taken {
795 Ok(batches) => {
796 for batch in batches {
797 let encoded =
798 tap_batch_record(&self.device, target, &batch, &self.wall)
799 .and_then(|record| canonical_bytes(&record, "tap.batch"));
800 match encoded {
801 Ok(bytes) => entry.pending.push_back(bytes),
802 Err(error) => {
803 failed(
806 &self.counts.console_records_lost,
807 1,
808 ConsoleFailure::ConsoleRecordsLost,
809 );
810 first_error.get_or_insert(error);
811 }
812 }
813 }
814 }
815 Err(error) => {
816 first_error.get_or_insert_with(|| error.into());
817 }
818 }
819 }
820 if !entry.pending.is_empty() {
821 due.push((
822 subject.clone(),
823 entry.generation,
824 std::mem::take(&mut entry.pending),
825 ));
826 }
827 }
828 drop(taps);
829
830 let mut unsent = Vec::new();
831 for (subject, generation, mut records) in due {
832 while let Some(bytes) = records.front() {
833 if !self.send(out, &subject, bytes) {
834 report.publish_failures += 1;
835 unsent.push((subject, generation, records));
836 break;
837 }
838 records.pop_front();
839 add(&self.counts.tap_records_published, 1);
840 report.tap_records += 1;
841 }
842 }
843
844 let mut taps = lock(&self.taps, "console taps")?;
845 for (subject, generation, records) in unsent {
846 match taps.live.get_mut(&subject) {
849 Some(entry) if entry.generation == generation => entry.pending = records,
850 _ => add(&self.counts.tap_records_abandoned, len_u64(records.len())),
851 }
852 }
853 let pending = pending_total(&taps.live);
854 drop(taps);
855 set(&self.counts.console_pending_records, pending);
856 first_error.map_or(Ok(()), Err)
857 }
858
859 fn retire(
860 &self,
861 out: &dyn ServerPublisher,
862 report: &mut PumpReport,
863 ) -> Result<(), ConsoleError> {
864 let mut retiring = std::mem::take(&mut lock(&self.taps, "console taps")?.retiring);
865 while let Some(subject) = retiring.first() {
866 let mut taps = lock(&self.taps, "console taps")?;
870 if taps.live.contains_key(subject) || taps.reserved.contains_key(subject) {
872 drop(taps);
873 retiring.remove(0);
874 continue;
875 }
876 if let Err(error) = out.retire_console_channel(subject) {
877 retiring.retain(|subject| !taps.live.contains_key(subject));
879 retiring.append(&mut taps.retiring);
880 taps.retiring = retiring;
881 drop(taps);
882 failed(
883 &self.counts.tap_detach_failures,
884 1,
885 ConsoleFailure::TapDetach,
886 );
887 return Err(error);
888 }
889 drop(taps);
890 retiring.remove(0);
891 report.retired += 1;
892 }
893 Ok(())
894 }
895}
896
897fn len_u64(len: usize) -> u64 {
898 u64::try_from(len).unwrap_or(u64::MAX)
899}
900
901fn pending_total(live: &BTreeMap<String, AttachedTap>) -> u64 {
902 live.values()
903 .map(|entry| len_u64(entry.pending.len()))
904 .sum()
905}