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            } 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    /// Whether a new subscription to `channel` is within the live-tap limit:
495    /// always for a channel that is not a tap or whose tap is live.
496    ///
497    /// # Errors
498    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
499    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    /// Admits a subscribe to `channel` against [`MAX_LIVE_TAPS`] and, for a
510    /// tap not yet live, reserves its slot in the same lock, so two racing
511    /// subscribes cannot both take the last slot. Answers false when no slot
512    /// is left. The attach that follows takes the reservation up;
513    /// [`Self::release_tap`] gives it back when the subscribe fails.
514    ///
515    /// # Errors
516    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
517    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    /// Gives back one reservation of `channel` for a subscribe that failed
538    /// after admission. When it was the last and no tap is live there, the
539    /// tap channel admission registered is taken off the roster by `retire`,
540    /// under the taps lock, so no subscribe reserving the tap again can see
541    /// its entry go: a refused subscribe leaves the roster as it found it. A
542    /// failed `retire` queues the subject for the pump; a poisoned table is
543    /// counted as a detach failure.
544    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    /// Counts a subscription to `channel`. A new `console.topology`
585    /// subscriber makes the latest document due again, so it is sent the
586    /// whole topology (R7); the first to a `console.tap.<target>` starts that
587    /// target's tap. Any other channel is ignored.
588    ///
589    /// # Errors
590    /// Returns [`ConsoleError::TapLimit`] when a new tap would pass
591    /// [`MAX_LIVE_TAPS`], or [`ConsoleError::Tap`] when a lock is poisoned;
592    /// either is counted in `tap_attach_failures`.
593    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    /// Uncounts a subscription to `channel`. The last one to leave a
654    /// `console.tap.<target>` stops that target's tap and queues the channel
655    /// for roster removal by the next pump. Safe on the channel actor's
656    /// thread: it takes only hub locks and wakes without waiting.
657    ///
658    /// # Errors
659    /// Returns [`TapError::Poisoned`] when a lock is poisoned; counted in
660    /// `tap_detach_failures`.
661    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        // R9: its pending records and unread ring leave with the tap, since
690        // nobody is left to read them; each is counted by name.
691        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    /// Subjects with a live tap, sorted.
711    ///
712    /// # Errors
713    /// Returns [`TapError::Poisoned`] when the tap table is poisoned.
714    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    /// Publishes what is due through `out`: the topology document when it
723    /// changed and someone subscribes, then each live tap's records, a
724    /// tap's kept records first and its ring drained only when none are
725    /// kept; then removes retired tap channels from the roster. A failed
726    /// publish keeps that record and the tap's later ones for the next pump,
727    /// and the pump goes on to the other taps.
728    ///
729    /// # Errors
730    /// Returns [`ConsoleError`] when a lock is poisoned or a record cannot be
731    /// encoded; publish failures are counted and reported, not returned.
732    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        // The taps pump whatever the topology did, so one failure there
736        // never holds the feed.
737        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            // Named on the record itself; the pump goes on.
757            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            // Due again: the next pump sends the document as it stands then.
771            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        // R9: one tap's failure is kept and returned after every other tap
783        // has been drained and published; it never drops what they took.
784        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                                    // Taken from the ring and not encodable:
803                                    // the one loss besides eviction, counted.
804                                    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            // Only to the tap they were taken from: one that detached
846            // meanwhile, or was attached anew, has no reader for them.
847            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            // The check and the roster removal share the taps lock, taken
866            // before the roster's, so no subscribe can reserve or attach the
867            // tap between them and find its channel gone (SRV-012 R6).
868            let mut taps = lock(&self.taps, "console taps")?;
869            // Attached or reserved again since it was queued: it stays.
870            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                // Kept for the next pump unless it was attached again since.
877                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}