1use 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
19pub const DEFAULT_TAP_CAPACITY: usize = 4096;
21
22pub 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
30pub type TapWaker = Arc<dyn Fn() + Send + Sync>;
33
34#[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 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 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 #[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 stations: Mutex<HashMap<u64, Arc<str>>>,
99 receivers: Mutex<HashMap<u64, u64>>,
102 buffers: Mutex<Vec<PublishRecord>>,
105 lent: AtomicU64,
107 no_stations: Arc<[Reached]>,
111 waker: OnceLock<TapWaker>,
112 capacity: AtomicUsize,
114}
115
116impl std::fmt::Debug for TapShared {
117 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 formatter
119 .debug_struct("TapShared")
120 .field("active", &self.active)
121 .field("observed", &self.observed)
122 .field("dropped_total", &self.dropped_total)
123 .finish_non_exhaustive()
124 }
125}
126
127#[derive(Clone, Debug)]
129pub struct Tap {
130 shared: Arc<TapShared>,
131 node: Arc<str>,
132 depot: Option<Arc<str>>,
135}
136
137impl Tap {
138 #[must_use]
142 pub fn new(node: &str, capacity: usize) -> Self {
143 Self::build(node, capacity, OnceLock::new())
144 }
145
146 #[must_use]
150 pub fn with_waker(node: &str, capacity: usize, waker: TapWaker) -> Self {
151 Self::build(node, capacity, OnceLock::from(waker))
152 }
153
154 #[cfg(any(test, feature = "test-support"))]
156 pub fn set_capacity(&self, capacity: usize) {
157 self.shared
158 .capacity
159 .store(capacity.max(1), Ordering::Relaxed);
160 }
161
162 fn build(node: &str, capacity: usize, waker: OnceLock<TapWaker>) -> Self {
163 Self {
164 shared: Arc::new(TapShared {
165 waker,
166 capacity: AtomicUsize::new(capacity.max(1)),
167 ..TapShared::default()
168 }),
169 node: Arc::from(node),
170 depot: None,
171 }
172 }
173
174 #[must_use]
178 pub fn with_depot(mut self, depot: bool) -> Self {
179 self.depot = depot.then(|| Arc::from(format!("{}.depot", self.node)));
180 self
181 }
182
183 #[must_use]
186 pub fn reserved(&self, id: &str) -> Option<&'static str> {
187 if id == &*self.node {
188 Some("exchange")
189 } else if self.depot.as_deref() == Some(id) {
190 Some("depot")
191 } else {
192 None
193 }
194 }
195
196 pub fn subscribe(&self, filter: TapFilter) -> Result<TapSubscription, TapError> {
201 self.subscribe_with(filter, None)
202 }
203
204 pub fn subscribe_with(
209 &self,
210 filter: TapFilter,
211 grant: Option<PayloadGrant>,
212 ) -> Result<TapSubscription, TapError> {
213 let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
214 let subscriber = Arc::new(Subscriber {
215 id,
216 filter,
217 grant,
218 state: Mutex::new(SubscriberState {
219 ring: BoundedRing::new(self.shared.capacity.load(Ordering::Relaxed)),
220 one_in: NonZeroU32::MIN,
221 seen: 0,
222 next_seq: 1,
223 }),
224 });
225 let mut subscribers = lock(&self.shared.subscribers, "tap subscribers")?;
226 subscribers.push(Arc::clone(&subscriber));
227 self.shared
228 .active
229 .store(subscribers.len(), Ordering::Release);
230 drop(subscribers);
231 Ok(TapSubscription {
232 shared: Arc::clone(&self.shared),
233 subscriber,
234 })
235 }
236
237 #[must_use]
241 pub fn observations(&self) -> u64 {
242 self.shared.observed.load(Ordering::Relaxed)
243 }
244
245 #[cfg(test)]
249 #[must_use]
250 pub(crate) fn server_observations(&self) -> u64 {
251 self.shared.observed_server.load(Ordering::Relaxed)
252 }
253
254 #[must_use]
256 pub fn subscriber_count(&self) -> usize {
257 self.shared.active.load(Ordering::Acquire)
258 }
259
260 #[must_use]
262 pub fn dropped_total(&self) -> u64 {
263 self.shared.dropped_total.load(Ordering::Relaxed)
264 }
265
266 #[cfg(test)]
269 pub(crate) fn poison_subscribers(&self) -> bool {
270 std::thread::scope(|scope| {
271 scope
272 .spawn(|| {
273 let _held = self.shared.subscribers.lock();
274 std::panic::resume_unwind(Box::new("poisoned for a test"));
275 })
276 .join()
277 .is_err()
278 }) && self.shared.subscribers.is_poisoned()
279 }
280
281 pub fn name_station(&self, connection: u64, station: &str) -> Result<(), TapError> {
287 lock(&self.shared.stations, "tap stations")?.insert(connection, Arc::from(station));
288 Ok(())
289 }
290
291 pub fn forget_station(&self, connection: u64) -> Result<(), TapError> {
296 lock(&self.shared.stations, "tap stations")?.remove(&connection);
297 Ok(())
298 }
299
300 pub fn name_receiver(&self, subscriber: u64, connection: u64) -> Result<(), TapError> {
306 lock(&self.shared.receivers, "tap receivers")?.insert(subscriber, connection);
307 Ok(())
308 }
309
310 pub fn forget_receiver(&self, subscriber: u64) -> Result<(), TapError> {
316 lock(&self.shared.receivers, "tap receivers")?.remove(&subscriber);
317 Ok(())
318 }
319
320 pub fn forget_receivers_of(&self, connection: u64) -> Result<(), TapError> {
325 lock(&self.shared.receivers, "tap receivers")?.retain(|_, owner| *owner != connection);
326 Ok(())
327 }
328
329 pub fn lend_recipients(&self) -> Result<Option<PublishRecord>, TapError> {
338 if self.shared.active.load(Ordering::Acquire) == 0 {
339 return Ok(None);
340 }
341 self.shared.lent.fetch_add(1, Ordering::Relaxed);
342 Ok(Some(
343 lock(&self.shared.buffers, "tap buffers")?
344 .pop()
345 .unwrap_or_default(),
346 ))
347 }
348
349 pub fn give_back(&self, mut buffer: PublishRecord) -> Result<(), TapError> {
355 buffer.clear();
356 lock(&self.shared.buffers, "tap buffers")?.push(buffer);
357 Ok(())
358 }
359
360 #[must_use]
363 pub fn recipients_lent(&self) -> u64 {
364 self.shared.lent.load(Ordering::Relaxed)
365 }
366
367 #[must_use]
370 pub fn unsubscribe_failures(&self) -> u64 {
371 self.shared.unsubscribe_failures.load(Ordering::Relaxed)
372 }
373}
374
375#[derive(Debug)]
377pub struct TapSubscription {
378 shared: Arc<TapShared>,
379 subscriber: Arc<Subscriber>,
380}
381
382fn elapsed_ms(elapsed: std::time::Duration) -> Option<u64> {
385 u64::try_from(elapsed.as_millis()).ok()
386}
387
388fn gather<'station>(
396 recipients: usize,
397 named: usize,
398 first_at: impl Fn(usize) -> Option<(&'station Arc<str>, Instant)>,
399) -> Result<Option<Arc<[Reached]>>, TapError> {
400 let Some(first) = (0..recipients).find_map(&first_at) else {
401 return if named == 0 {
402 Ok(None)
403 } else {
404 Err(TapError::StationsDisagree { named, found: 0 })
405 };
406 };
407 let mut next = 0;
412 let mut found = 0_usize;
413 let reached = |(station, queued_at): (&Arc<str>, Instant)| Reached {
414 station: Arc::clone(station),
415 queued_at,
416 };
417 let list: Arc<[Reached]> = (0..named)
418 .map(|_| {
419 match (next..recipients)
420 .find_map(|index| first_at(index).map(|station| (index, station)))
421 {
422 Some((index, station)) => {
423 next = index + 1;
424 found += 1;
425 reached(station)
426 }
427 None => reached(first),
428 }
429 })
430 .collect();
431 let beyond = (next..recipients)
432 .filter(|index| first_at(*index).is_some())
433 .count();
434 if found != named || beyond != 0 {
435 return Err(TapError::StationsDisagree {
436 named,
437 found: found + beyond,
438 });
439 }
440 Ok(Some(list))
441}
442
443#[cfg(test)]
444mod gather_tests {
445 use std::sync::Arc;
446 use std::time::{Duration, Instant};
447
448 use super::{Reached, TapError, elapsed_ms, gather};
449
450 #[test]
453 fn an_elapsed_time_beyond_u64_milliseconds_has_no_reading() {
454 assert_eq!(elapsed_ms(Duration::from_millis(7)), Some(7));
455 assert_eq!(elapsed_ms(Duration::MAX), None);
456 }
457
458 #[test]
464 fn a_fill_that_disagrees_with_the_count_is_refused() {
465 let ledger: Arc<str> = Arc::from("orders.ledger");
466 let queued = Instant::now();
467 let gathered = gather(2, 2, |index| (index == 0).then_some((&ledger, queued)));
468 assert!(
469 matches!(
470 gathered,
471 Err(TapError::StationsDisagree { named: 2, found: 1 })
472 ),
473 "{gathered:?}"
474 );
475 }
476
477 #[test]
480 fn a_fill_that_agrees_builds_the_list() -> Result<(), TapError> {
481 let ledger: Arc<str> = Arc::from("orders.ledger");
482 let audit: Arc<str> = Arc::from("orders.audit");
483 let queued = Instant::now();
484 let first_at = |index: usize| match index {
485 0 => Some((&ledger, queued)),
486 2 => Some((&audit, queued)),
487 _ => None,
488 };
489 let reached = |station: &Arc<str>| Reached {
490 station: Arc::clone(station),
491 queued_at: queued,
492 };
493 assert_eq!(
494 gather(3, 2, first_at)?,
495 Some(Arc::from(vec![reached(&ledger), reached(&audit)]))
496 );
497 assert_eq!(gather(3, 0, |_| None)?, None);
498 Ok(())
499 }
500}