Skip to main content

liminal_server/console/
hub.rs

1//! The console hub the connection services own (SRV-012 R4, R6, R7, R9).
2//!
3//! It holds the topology, the tap, one tap subscription per
4//! `console.tap.<target>` channel with at least one subscriber, and the pump
5//! that turns them into records.
6//!
7//! The pump runs on the publisher's thread and never publishes while it holds
8//! a hub lock: it takes what is due under the lock, lets go, publishes, and
9//! puts back what failed. A tap with a pending record is not drained, so its
10//! ring stays the only buffer and ring eviction the only loss, which the next
11//! batch's `dropped` reports. One tap's failure never stops the others: it
12//! is returned once every tap has been drained, published and retired. The
13//! pending records and unread ring of a tap whose last subscriber left go
14//! with it, as do the unsent records of a tap attached anew before they were
15//! put back; nobody is left to read them, so they are counted by name in
16//! `tap_records_abandoned` and `tap_messages_abandoned`, not in
17//! `console_records_lost`.
18
19use 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
32/// The topology stream's subject.
33pub const TOPOLOGY_SUBJECT: &str = "console.topology";
34/// The prefix of every tap stream's subject; the rest names the target.
35pub const TAP_SUBJECT_PREFIX: &str = "console.tap.";
36/// The most taps live at once (R6); one more is `CONSOLE_TAP_LIMIT_REFUSED`.
37pub 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    /// Which attach of this subject it is, so the pump never puts an earlier
52    /// tap's unsent records back into a later one (SRV-012 R9).
53    generation: u64,
54}
55
56#[derive(Debug, Default)]
57struct Taps {
58    live: BTreeMap<String, AttachedTap>,
59    /// New taps admitted and not yet attached, with how many subscribes hold
60    /// each: they count against [`MAX_LIVE_TAPS`] from admission on.
61    reserved: BTreeMap<String, u64>,
62    retiring: Vec<String>,
63    /// The next tap's [`AttachedTap::generation`].
64    generations: u64,
65}
66
67type NotifierSlot = Arc<Mutex<Option<TapWaker>>>;
68
69/// One of the hub's locks, named for a test that poisons it.
70#[cfg(test)]
71#[derive(Clone, Copy, Debug)]
72pub(crate) enum HubLock {
73    /// The held topology.
74    Topology,
75    /// The live taps.
76    Taps,
77    /// The publisher's notifier slot.
78    Notifier,
79}
80
81/// What one pump did.
82#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
83pub struct PumpReport {
84    /// `console.topology` records published.
85    pub topology_records: u64,
86    /// `console.tap.<target>` records published.
87    pub tap_records: u64,
88    /// Publishes that failed; each record is kept for the next pump.
89    pub publish_failures: u64,
90    /// Tap channels removed from the roster.
91    pub retired: u64,
92}
93
94/// The console's state, owned by the connection services.
95pub 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    /// Every record the publisher took, as the encoder made it. Test-only.
104    #[cfg(test)]
105    sent: Mutex<Vec<(String, Vec<u8>)>>,
106    /// How many topology changes reached [`Self::emit`]. Test-only.
107    #[cfg(test)]
108    emits: std::sync::atomic::AtomicU64,
109    /// How many times the topology lock was taken. Test-only.
110    #[cfg(test)]
111    topology_locks: std::sync::atomic::AtomicU64,
112    /// Notified after each applied topology change. Test-only.
113    #[cfg(test)]
114    applied: std::sync::Condvar,
115    /// A tap subject whose next take fails, as a poisoned ring would.
116    /// Test-only.
117    #[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
131/// A waker that forwards to whatever notifier is installed when it fires. A
132/// poisoned slot is refused, not recovered: the wake is counted as failed.
133fn 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/// Whether the tap hook may observe a publish on `subject`: never one under
151/// `console.` (the console's own records).
152#[must_use]
153pub fn tappable(subject: &str) -> bool {
154    !crate::server::connection::is_console_subject(subject)
155}
156
157impl ConsoleHub {
158    /// A hub for `device` holding `model`, timing records from `wall`, with
159    /// a tap of the default capacity.
160    ///
161    /// # Errors
162    /// Returns [`ConsoleError::EmptyDevice`] when `device` is blank.
163    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    /// As [`Self::new`], with each tap subscriber's ring holding `capacity`.
168    ///
169    /// # Errors
170    /// Returns [`ConsoleError::EmptyDevice`] when `device` is blank.
171    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(&notifier), 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    /// The device this hub publishes for (`liminal.<device>`).
214    #[must_use]
215    pub fn device(&self) -> &str {
216        &self.device
217    }
218
219    /// The tap.
220    #[must_use]
221    pub const fn tap(&self) -> &Tap {
222        &self.tap
223    }
224
225    /// Every console counter as it stands.
226    #[must_use]
227    pub fn counts(&self) -> ConsoleCountsSnapshot {
228        self.counts
229            .snapshot(self.tap.dropped_total(), self.tap.unsubscribe_failures())
230    }
231
232    /// Publishes one encoded record and answers whether the publisher took
233    /// it; a refusal is one `console_publish` failure.
234    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    /// How many topology changes reached [`Self::emit`]. Test-only.
251    #[cfg(test)]
252    pub(crate) fn emits(&self) -> u64 {
253        self.emits.load(std::sync::atomic::Ordering::Relaxed)
254    }
255
256    /// Each connection's participant as the model holds it. Test-only.
257    #[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    /// How many times the topology lock was taken. Test-only.
266    #[cfg(test)]
267    pub(crate) fn topology_locks(&self) -> u64 {
268        self.topology_locks
269            .load(std::sync::atomic::Ordering::Relaxed)
270    }
271
272    /// Waits until the model holds `count` connections and answers whether
273    /// it does; the guard is a minute, never a deadline. Test-only.
274    #[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    /// Makes the next take of `subject`'s tap fail once. Test-only.
290    #[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    /// Takes `subscription`'s batches, unless [`Self::fail_next_take`] named
298    /// `subject`: that take fails once. Test-only.
299    #[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    /// The topology lock, counted under test.
317    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    /// This server's incarnation started at `started`; the topology's uptime
325    /// runs from there and the next document says so.
326    ///
327    /// # Errors
328    /// Returns [`TapError::Poisoned`] when the topology lock is poisoned.
329    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    /// Counts a dedup-suppressed re-publish, which is not tapped.
339    pub fn suppressed(&self) {
340        add(&self.counts.tap_suppressed_total, 1);
341    }
342
343    /// Whether the taps lock is held at this moment. Test-only.
344    #[cfg(test)]
345    pub(crate) fn taps_held(&self) -> bool {
346        self.taps.try_lock().is_err()
347    }
348
349    /// How many tap channels hold an admission reservation. Test-only.
350    #[cfg(test)]
351    pub(crate) fn reserved_taps(&self) -> Option<usize> {
352        self.taps.lock().ok().map(|taps| taps.reserved.len())
353    }
354
355    /// Every record the publisher took, oldest first, as the encoder made
356    /// it; `None` when the capture is poisoned. Test-only.
357    #[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    /// Poisons one of the hub's locks, as a panic on a thread holding it
363    /// would, and answers whether it is now poisoned. Test-only.
364    #[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    /// Counts a publish the tap could not observe (its lock was poisoned).
386    /// The publish itself answered as it would without the console.
387    pub fn observe_failed(&self) {
388        failed(
389            &self.counts.tap_observe_failures,
390            1,
391            ConsoleFailure::TapObserve,
392        );
393    }
394
395    /// Installs the function that tells the publisher there is work. The
396    /// publisher calls [`Self::pump`] on its own thread when told.
397    ///
398    /// # Errors
399    /// Returns [`TapError::Poisoned`] when the slot is poisoned.
400    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    /// Wakes the publisher when work is already waiting: a topology
410    /// subscriber, a live tap or a tap to retire. The start of the console
411    /// streams calls it, so a subscriber that arrived before the publisher
412    /// ran is served, and an idle server is never woken.
413    ///
414    /// # Errors
415    /// Returns [`TapError::Poisoned`] when a hub lock is poisoned.
416    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    /// Applies `change` to the held topology. A change that alters it makes
429    /// the document due, and wakes the publisher when someone subscribes.
430    ///
431    /// # Errors
432    /// Returns [`TapError::Poisoned`] when the topology lock is poisoned; the
433    /// change is counted in `topology_emit_failures`.
434    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        // A worker identity or service subject that is this server's exchange
446        // or depot id is kept off the map and counted (Buttercup's V1, Hermes
447        // 04:27): the registration stands, merged into nothing.
448        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        // A registered worker's tapped publishes name its station from here
466        // to its connection's close.
467        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    /// Whether a new subscription to `channel` is within the live-tap limit:
496    /// always for a channel that is not a tap or whose tap is live.
497    ///
498    /// # Errors
499    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
500    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    /// Admits a subscribe to `channel` against [`MAX_LIVE_TAPS`] and, for a
511    /// tap not yet live, reserves its slot in the same lock, so two racing
512    /// subscribes cannot both take the last slot. Answers false when no slot
513    /// is left. The attach that follows takes the reservation up;
514    /// [`Self::release_tap`] gives it back when the subscribe fails.
515    ///
516    /// # Errors
517    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
518    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    /// Gives back one reservation of `channel` for a subscribe that failed
539    /// after admission. When it was the last and no tap is live there, the
540    /// tap channel admission registered is taken off the roster by `retire`,
541    /// under the taps lock, so no subscribe reserving the tap again can see
542    /// its entry go: a refused subscribe leaves the roster as it found it. A
543    /// failed `retire` queues the subject for the pump; a poisoned table is
544    /// counted as a detach failure.
545    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    /// Counts a subscription to `channel`. A new `console.topology`
586    /// subscriber makes the latest document due again, so it is sent the
587    /// whole topology (R7); the first to a `console.tap.<target>` starts that
588    /// target's tap. Any other channel is ignored.
589    ///
590    /// # Errors
591    /// Returns [`ConsoleError::TapLimit`] when a new tap would pass
592    /// [`MAX_LIVE_TAPS`], or [`ConsoleError::Tap`] when a lock is poisoned;
593    /// either is counted in `tap_attach_failures`.
594    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    /// Uncounts a subscription to `channel`. The last one to leave a
655    /// `console.tap.<target>` stops that target's tap and queues the channel
656    /// for roster removal by the next pump. Safe on the channel actor's
657    /// thread: it takes only hub locks and wakes without waiting.
658    ///
659    /// # Errors
660    /// Returns [`TapError::Poisoned`] when a lock is poisoned; counted in
661    /// `tap_detach_failures`.
662    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        // R9: its pending records and unread ring leave with the tap, since
691        // nobody is left to read them; each is counted by name.
692        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    /// Subjects with a live tap, sorted.
712    ///
713    /// # Errors
714    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
715    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    /// Publishes what is due through `out`: the topology document when it
724    /// changed and someone subscribes, then each live tap's records, a
725    /// tap's kept records first and its ring drained only when none are
726    /// kept; then removes retired tap channels from the roster. A failed
727    /// publish keeps that record and the tap's later ones for the next pump,
728    /// and the pump goes on to the other taps.
729    ///
730    /// # Errors
731    /// Returns [`ConsoleError`] when a lock is poisoned or a record cannot be
732    /// encoded; publish failures are counted and reported, not returned.
733    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        // The taps pump whatever the topology did, so one failure there
737        // never holds the feed.
738        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            // Named on the record itself; the pump goes on.
758            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            // Due again: the next pump sends the document as it stands then.
772            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        // R9: one tap's failure is kept and returned after every other tap
784        // has been drained and published; it never drops what they took.
785        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                                    // Taken from the ring and not encodable:
804                                    // the one loss besides eviction, counted.
805                                    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            // Only to the tap they were taken from: one that detached
847            // meanwhile, or was attached anew, has no reader for them.
848            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            // The check and the roster removal share the taps lock, taken
867            // before the roster's, so no subscribe can reserve or attach the
868            // tap between them and find its channel gone (SRV-012 R6).
869            let mut taps = lock(&self.taps, "console taps")?;
870            // Attached or reserved again since it was queued: it stays.
871            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                // Kept for the next pump unless it was attached again since.
878                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}