Skip to main content

liminal_server/tap/
subscription.rs

1//! The tap and its subscriptions.
2
3use liminal::channel::{PublishRecord, Recipient};
4use std::collections::HashMap;
5use std::num::NonZeroU32;
6use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7use std::sync::{Arc, Mutex, OnceLock};
8use std::time::Instant;
9
10use super::error::{TapError, lock};
11use super::message::{
12    Hop, PayloadGrant, PublishFact, Reached, TapBatch, TapFilter, TapIdent, TapMessage, TapSource,
13};
14use super::ring::BoundedRing;
15mod call;
16mod publish;
17mod reader;
18
19/// Default per-subscriber ring capacity, in messages.
20pub const DEFAULT_TAP_CAPACITY: usize = 4096;
21
22/// The largest sampling divisor the server will choose.
23pub const MAX_SAMPLE_ONE_IN: NonZeroU32 = match NonZeroU32::new(1 << 16) {
24    Some(most) => most,
25    None => NonZeroU32::MIN,
26};
27
28const TWO: NonZeroU32 = NonZeroU32::MIN.saturating_add(1);
29
30/// Wakes whoever drains the tap; called at most once per publish, after every
31/// tap lock is released.
32pub type TapWaker = Arc<dyn Fn() + Send + Sync>;
33
34/// One kept message and the divisor it was admitted under, so a drain can
35/// close a batch wherever the divisor changed without a second ring.
36#[derive(Debug)]
37struct Admitted {
38    one_in: NonZeroU32,
39    message: TapMessage,
40}
41
42#[derive(Debug)]
43struct SubscriberState {
44    ring: BoundedRing<Admitted>,
45    one_in: NonZeroU32,
46    seen: u64,
47    next_seq: u64,
48}
49
50impl SubscriberState {
51    /// The sampling decision for one matching publish, taken before anything
52    /// is allocated for it.
53    fn admits(&mut self) -> bool {
54        let take = self.seen % u64::from(self.one_in.get()) == 0;
55        self.seen = self.seen.wrapping_add(1);
56        take
57    }
58
59    /// Doubles the divisor once the ring is more than half full.
60    fn after_push(&mut self) {
61        if self.ring.len().saturating_mul(2) > self.ring.capacity() {
62            self.one_in = self.one_in.saturating_mul(TWO).min(MAX_SAMPLE_ONE_IN);
63        }
64    }
65}
66
67struct Subscriber {
68    id: u64,
69    filter: TapFilter,
70    grant: Option<PayloadGrant>,
71    state: Mutex<SubscriberState>,
72}
73
74impl std::fmt::Debug for Subscriber {
75    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        formatter
77            .debug_struct("Subscriber")
78            .field("id", &self.id)
79            .field("filter", &self.filter)
80            .finish_non_exhaustive()
81    }
82}
83
84#[derive(Default)]
85struct TapShared {
86    active: AtomicUsize,
87    observed: AtomicU64,
88    /// Of `observed`, the server's own publishes (`liminal.<device>`).
89    /// Test-only.
90    #[cfg(test)]
91    observed_server: AtomicU64,
92    dropped_total: AtomicU64,
93    unsubscribe_failures: AtomicU64,
94    next_id: AtomicU64,
95    subscribers: Mutex<Vec<Arc<Subscriber>>>,
96    /// Each registered worker's connection and its station id, so a kept
97    /// message names its publishing station without the topology lock.
98    stations: Mutex<HashMap<u64, Arc<str>>>,
99    /// Each live subscription's subscriber pid and its connection, so a
100    /// recipient the fan-out recorded names its connection's station.
101    receivers: Mutex<HashMap<u64, u64>>,
102    /// Recipient buffers between publishes, so a recording publish reuses
103    /// an allocation rather than making one.
104    buffers: Mutex<Vec<PublishRecord>>,
105    /// Recipient buffers lent: publishes that recorded their recipients.
106    lent: AtomicU64,
107    /// The one empty station list, shared by every kept message whose
108    /// recorded fan-out reached no registered worker, so naming none
109    /// allocates nothing.
110    no_stations: Arc<[Reached]>,
111    waker: OnceLock<TapWaker>,
112}
113
114impl std::fmt::Debug for TapShared {
115    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        formatter
117            .debug_struct("TapShared")
118            .field("active", &self.active)
119            .field("observed", &self.observed)
120            .field("dropped_total", &self.dropped_total)
121            .finish_non_exhaustive()
122    }
123}
124
125/// The server's tap. Cheap to clone; every clone is the same tap.
126#[derive(Clone, Debug)]
127pub struct Tap {
128    shared: Arc<TapShared>,
129    node: Arc<str>,
130    /// This server's depot's station id, when it keeps a persistent store:
131    /// a durable message's hop between its publisher and the exchange.
132    depot: Option<Arc<str>>,
133    capacity: usize,
134}
135
136impl Tap {
137    /// A tap whose hops name `node`, with per-subscriber rings of `capacity`,
138    /// that wakes nobody: for a reader that drains when it chooses, as a test
139    /// does.
140    #[must_use]
141    pub fn new(node: &str, capacity: usize) -> Self {
142        Self::build(node, capacity, OnceLock::new())
143    }
144
145    /// A tap that calls `waker` once per publish any subscription kept.
146    /// Fixed at construction, so the hot path reads it without a lock and
147    /// nothing can replace it.
148    #[must_use]
149    pub fn with_waker(node: &str, capacity: usize, waker: TapWaker) -> Self {
150        Self::build(node, capacity, OnceLock::from(waker))
151    }
152
153    fn build(node: &str, capacity: usize, waker: OnceLock<TapWaker>) -> Self {
154        Self {
155            shared: Arc::new(TapShared {
156                waker,
157                ..TapShared::default()
158            }),
159            node: Arc::from(node),
160            depot: None,
161            capacity: capacity.max(1),
162        }
163    }
164
165    /// This tap, on a server that keeps a persistent store when `depot`:
166    /// its kept durable messages then pass `<node>.depot` (lane brief
167    /// ruling 8).
168    #[must_use]
169    pub fn with_depot(mut self, depot: bool) -> Self {
170        self.depot = depot.then(|| Arc::from(format!("{}.depot", self.node)));
171        self
172    }
173
174    /// The station `id` would collide with: this tap's exchange (its node)
175    /// or its depot. A worker with such an identity names no station here.
176    #[must_use]
177    pub fn reserved(&self, id: &str) -> Option<&'static str> {
178        if id == &*self.node {
179            Some("exchange")
180        } else if self.depot.as_deref() == Some(id) {
181            Some("depot")
182        } else {
183            None
184        }
185    }
186
187    /// Subscribes without payloads.
188    ///
189    /// # Errors
190    /// Returns [`TapError::Poisoned`] when the subscriber list is poisoned.
191    pub fn subscribe(&self, filter: TapFilter) -> Result<TapSubscription, TapError> {
192        self.subscribe_with(filter, None)
193    }
194
195    /// Subscribes, with payloads when `grant` is present.
196    ///
197    /// # Errors
198    /// Returns [`TapError::Poisoned`] when the subscriber list is poisoned.
199    pub fn subscribe_with(
200        &self,
201        filter: TapFilter,
202        grant: Option<PayloadGrant>,
203    ) -> Result<TapSubscription, TapError> {
204        let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
205        let subscriber = Arc::new(Subscriber {
206            id,
207            filter,
208            grant,
209            state: Mutex::new(SubscriberState {
210                ring: BoundedRing::new(self.capacity),
211                one_in: NonZeroU32::MIN,
212                seen: 0,
213                next_seq: 1,
214            }),
215        });
216        let mut subscribers = lock(&self.shared.subscribers, "tap subscribers")?;
217        subscribers.push(Arc::clone(&subscriber));
218        self.shared
219            .active
220            .store(subscribers.len(), Ordering::Release);
221        drop(subscribers);
222        Ok(TapSubscription {
223            shared: Arc::clone(&self.shared),
224            subscriber,
225        })
226    }
227
228    /// Publishes the hook has shown the tap while a subscription was live (a
229    /// count pin: N ordinary publishes give N, the tap's own records give
230    /// zero).
231    #[must_use]
232    pub fn observations(&self) -> u64 {
233        self.shared.observed.load(Ordering::Relaxed)
234    }
235
236    /// Of [`Self::observations`], those of the server's own publishes (its
237    /// `liminal.events` and `liminal.cost` rows), which its publisher owner
238    /// writes on its own schedule. Test-only.
239    #[cfg(test)]
240    #[must_use]
241    pub(crate) fn server_observations(&self) -> u64 {
242        self.shared.observed_server.load(Ordering::Relaxed)
243    }
244
245    /// Live subscriptions.
246    #[must_use]
247    pub fn subscriber_count(&self) -> usize {
248        self.shared.active.load(Ordering::Acquire)
249    }
250
251    /// Messages evicted from every subscription's ring since the tap was made.
252    #[must_use]
253    pub fn dropped_total(&self) -> u64 {
254        self.shared.dropped_total.load(Ordering::Relaxed)
255    }
256
257    /// Poisons the subscriber list, as a panic on a thread holding it would,
258    /// and answers whether it is now poisoned. Test-only.
259    #[cfg(test)]
260    pub(crate) fn poison_subscribers(&self) -> bool {
261        std::thread::scope(|scope| {
262            scope
263                .spawn(|| {
264                    let _held = self.shared.subscribers.lock();
265                    std::panic::resume_unwind(Box::new("poisoned for a test"));
266                })
267                .join()
268                .is_err()
269        }) && self.shared.subscribers.is_poisoned()
270    }
271
272    /// Names `station` as the station `connection` publishes as, from its
273    /// worker registration until [`Self::forget_station`].
274    ///
275    /// # Errors
276    /// Returns [`TapError::Poisoned`] when the station table is poisoned.
277    pub fn name_station(&self, connection: u64, station: &str) -> Result<(), TapError> {
278        lock(&self.shared.stations, "tap stations")?.insert(connection, Arc::from(station));
279        Ok(())
280    }
281
282    /// Forgets `connection`'s station, as its connection closes.
283    ///
284    /// # Errors
285    /// Returns [`TapError::Poisoned`] when the station table is poisoned.
286    pub fn forget_station(&self, connection: u64) -> Result<(), TapError> {
287        lock(&self.shared.stations, "tap stations")?.remove(&connection);
288        Ok(())
289    }
290
291    /// Names `connection` as the owner of the subscription whose subscriber
292    /// pid is `subscriber`, until [`Self::forget_receiver`].
293    ///
294    /// # Errors
295    /// Returns [`TapError::Poisoned`] when the receiver table is poisoned.
296    pub fn name_receiver(&self, subscriber: u64, connection: u64) -> Result<(), TapError> {
297        lock(&self.shared.receivers, "tap receivers")?.insert(subscriber, connection);
298        Ok(())
299    }
300
301    /// Forgets the subscription whose subscriber pid is `subscriber`, as it
302    /// leaves its channel.
303    ///
304    /// # Errors
305    /// Returns [`TapError::Poisoned`] when the receiver table is poisoned.
306    pub fn forget_receiver(&self, subscriber: u64) -> Result<(), TapError> {
307        lock(&self.shared.receivers, "tap receivers")?.remove(&subscriber);
308        Ok(())
309    }
310
311    /// Forgets every subscription of `connection`, as it closes.
312    ///
313    /// # Errors
314    /// Returns [`TapError::Poisoned`] when the receiver table is poisoned.
315    pub fn forget_receivers_of(&self, connection: u64) -> Result<(), TapError> {
316        lock(&self.shared.receivers, "tap receivers")?.retain(|_, owner| *owner != connection);
317        Ok(())
318    }
319
320    /// A buffer for a publish to record its recipients in, only while a
321    /// subscription is live: with none it is one atomic load, no lock and no
322    /// allocation, and the publish records nothing. A buffer lent comes from
323    /// those given back, so it allocates only while the pool and each buffer
324    /// grow to the widest concurrent fan-out.
325    ///
326    /// # Errors
327    /// Returns [`TapError::Poisoned`] when the buffer pool is poisoned.
328    pub fn lend_recipients(&self) -> Result<Option<PublishRecord>, TapError> {
329        if self.shared.active.load(Ordering::Acquire) == 0 {
330            return Ok(None);
331        }
332        self.shared.lent.fetch_add(1, Ordering::Relaxed);
333        Ok(Some(
334            lock(&self.shared.buffers, "tap buffers")?
335                .pop()
336                .unwrap_or_default(),
337        ))
338    }
339
340    /// Takes back a buffer [`Self::lend_recipients`] lent, emptied, for the
341    /// next recording publish.
342    ///
343    /// # Errors
344    /// Returns [`TapError::Poisoned`] when the buffer pool is poisoned.
345    pub fn give_back(&self, mut buffer: PublishRecord) -> Result<(), TapError> {
346        buffer.clear();
347        lock(&self.shared.buffers, "tap buffers")?.push(buffer);
348        Ok(())
349    }
350
351    /// Recipient buffers lent since the tap was made: publishes that asked
352    /// the fan-out for their recipients.
353    #[must_use]
354    pub fn recipients_lent(&self) -> u64 {
355        self.shared.lent.load(Ordering::Relaxed)
356    }
357
358    /// Subscriptions whose drop found the subscriber list poisoned and so
359    /// could not leave it.
360    #[must_use]
361    pub fn unsubscribe_failures(&self) -> u64 {
362        self.shared.unsubscribe_failures.load(Ordering::Relaxed)
363    }
364}
365
366/// One subscriber's handle. Dropping it unsubscribes.
367#[derive(Debug)]
368pub struct TapSubscription {
369    shared: Arc<TapShared>,
370    subscriber: Arc<Subscriber>,
371}
372
373/// `elapsed` in whole milliseconds; `None` beyond what a u64 holds, which
374/// the caller names rather than writing the most a u64 holds.
375fn elapsed_ms(elapsed: std::time::Duration) -> Option<u64> {
376    u64::try_from(elapsed.as_millis()).ok()
377}
378
379/// The `named` first-named stations of `recipients` recipients, in order, in
380/// one allocation: `first_at(index)` is the station the recipient at `index`
381/// names first, if it names one, with the instant that recipient was queued. `None` when none is named.
382///
383/// # Errors
384/// Returns [`TapError::StationsDisagree`] when the fill finds other than
385/// `named` stations.
386fn gather<'station>(
387    recipients: usize,
388    named: usize,
389    first_at: impl Fn(usize) -> Option<(&'station Arc<str>, Instant)>,
390) -> Result<Option<Arc<[Reached]>>, TapError> {
391    let Some(first) = (0..recipients).find_map(&first_at) else {
392        return if named == 0 {
393            Ok(None)
394        } else {
395            Err(TapError::StationsDisagree { named, found: 0 })
396        };
397    };
398    // A range mapped is of known length, so the collect allocates once.
399    // Each step finds the next first-named station. A step that finds none
400    // fills its slot with `first` only so the slice is whole; the count of
401    // those found then refuses the list, so that slot never leaves here.
402    let mut next = 0;
403    let mut found = 0_usize;
404    let reached = |(station, queued_at): (&Arc<str>, Instant)| Reached {
405        station: Arc::clone(station),
406        queued_at,
407    };
408    let list: Arc<[Reached]> = (0..named)
409        .map(|_| {
410            match (next..recipients)
411                .find_map(|index| first_at(index).map(|station| (index, station)))
412            {
413                Some((index, station)) => {
414                    next = index + 1;
415                    found += 1;
416                    reached(station)
417                }
418                None => reached(first),
419            }
420        })
421        .collect();
422    let beyond = (next..recipients)
423        .filter(|index| first_at(*index).is_some())
424        .count();
425    if found != named || beyond != 0 {
426        return Err(TapError::StationsDisagree {
427            named,
428            found: found + beyond,
429        });
430    }
431    Ok(Some(list))
432}
433
434#[cfg(test)]
435mod gather_tests {
436    use std::sync::Arc;
437    use std::time::{Duration, Instant};
438
439    use super::{Reached, TapError, elapsed_ms, gather};
440
441    /// Buttercup's V2, Hermes 04:27: a hop further out than u64
442    /// milliseconds is named, never written as the most a u64 holds.
443    #[test]
444    fn an_elapsed_time_beyond_u64_milliseconds_has_no_reading() {
445        assert_eq!(elapsed_ms(Duration::from_millis(7)), Some(7));
446        assert_eq!(elapsed_ms(Duration::MAX), None);
447    }
448
449    /// Buttercup's finding 2 (Hermes 03:05): when the filling pass finds
450    /// fewer stations than the count named, the list is refused by name,
451    /// never filled in with a station that stands in for the missing one.
452    /// The count named two; the fill finds only the ledger, as if the audit
453    /// worker had left between the passes.
454    #[test]
455    fn a_fill_that_disagrees_with_the_count_is_refused() {
456        let ledger: Arc<str> = Arc::from("orders.ledger");
457        let queued = Instant::now();
458        let gathered = gather(2, 2, |index| (index == 0).then_some((&ledger, queued)));
459        assert!(
460            matches!(
461                gathered,
462                Err(TapError::StationsDisagree { named: 2, found: 1 })
463            ),
464            "{gathered:?}"
465        );
466    }
467
468    /// Positive control: passes that agree build the list, and none named
469    /// builds none.
470    #[test]
471    fn a_fill_that_agrees_builds_the_list() -> Result<(), TapError> {
472        let ledger: Arc<str> = Arc::from("orders.ledger");
473        let audit: Arc<str> = Arc::from("orders.audit");
474        let queued = Instant::now();
475        let first_at = |index: usize| match index {
476            0 => Some((&ledger, queued)),
477            2 => Some((&audit, queued)),
478            _ => None,
479        };
480        let reached = |station: &Arc<str>| Reached {
481            station: Arc::clone(station),
482            queued_at: queued,
483        };
484        assert_eq!(
485            gather(3, 2, first_at)?,
486            Some(Arc::from(vec![reached(&ledger), reached(&audit)]))
487        );
488        assert_eq!(gather(3, 0, |_| None)?, None);
489        Ok(())
490    }
491}