use liminal::channel::{PublishRecord, Recipient};
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Instant;
use super::error::{TapError, lock};
use super::message::{
Hop, PayloadGrant, PublishFact, Reached, TapBatch, TapFilter, TapIdent, TapMessage, TapSource,
};
use super::ring::BoundedRing;
mod call;
mod publish;
mod reader;
pub const DEFAULT_TAP_CAPACITY: usize = 4096;
pub const MAX_SAMPLE_ONE_IN: NonZeroU32 = match NonZeroU32::new(1 << 16) {
Some(most) => most,
None => NonZeroU32::MIN,
};
const TWO: NonZeroU32 = NonZeroU32::MIN.saturating_add(1);
pub type TapWaker = Arc<dyn Fn() + Send + Sync>;
#[derive(Debug)]
struct Admitted {
one_in: NonZeroU32,
message: TapMessage,
}
#[derive(Debug)]
struct SubscriberState {
ring: BoundedRing<Admitted>,
one_in: NonZeroU32,
seen: u64,
next_seq: u64,
}
impl SubscriberState {
fn admits(&mut self) -> bool {
let take = self.seen % u64::from(self.one_in.get()) == 0;
self.seen = self.seen.wrapping_add(1);
take
}
fn after_push(&mut self) {
if self.ring.len().saturating_mul(2) > self.ring.capacity() {
self.one_in = self.one_in.saturating_mul(TWO).min(MAX_SAMPLE_ONE_IN);
}
}
}
struct Subscriber {
id: u64,
filter: TapFilter,
grant: Option<PayloadGrant>,
state: Mutex<SubscriberState>,
}
impl std::fmt::Debug for Subscriber {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Subscriber")
.field("id", &self.id)
.field("filter", &self.filter)
.finish_non_exhaustive()
}
}
#[derive(Default)]
struct TapShared {
active: AtomicUsize,
observed: AtomicU64,
#[cfg(test)]
observed_server: AtomicU64,
dropped_total: AtomicU64,
unsubscribe_failures: AtomicU64,
next_id: AtomicU64,
subscribers: Mutex<Vec<Arc<Subscriber>>>,
stations: Mutex<HashMap<u64, Arc<str>>>,
receivers: Mutex<HashMap<u64, u64>>,
buffers: Mutex<Vec<PublishRecord>>,
lent: AtomicU64,
no_stations: Arc<[Reached]>,
waker: OnceLock<TapWaker>,
capacity: AtomicUsize,
}
impl std::fmt::Debug for TapShared {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("TapShared")
.field("active", &self.active)
.field("observed", &self.observed)
.field("dropped_total", &self.dropped_total)
.finish_non_exhaustive()
}
}
#[derive(Clone, Debug)]
pub struct Tap {
shared: Arc<TapShared>,
node: Arc<str>,
depot: Option<Arc<str>>,
}
impl Tap {
#[must_use]
pub fn new(node: &str, capacity: usize) -> Self {
Self::build(node, capacity, OnceLock::new())
}
#[must_use]
pub fn with_waker(node: &str, capacity: usize, waker: TapWaker) -> Self {
Self::build(node, capacity, OnceLock::from(waker))
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_capacity(&self, capacity: usize) {
self.shared
.capacity
.store(capacity.max(1), Ordering::Relaxed);
}
fn build(node: &str, capacity: usize, waker: OnceLock<TapWaker>) -> Self {
Self {
shared: Arc::new(TapShared {
waker,
capacity: AtomicUsize::new(capacity.max(1)),
..TapShared::default()
}),
node: Arc::from(node),
depot: None,
}
}
#[must_use]
pub fn with_depot(mut self, depot: bool) -> Self {
self.depot = depot.then(|| Arc::from(format!("{}.depot", self.node)));
self
}
#[must_use]
pub fn reserved(&self, id: &str) -> Option<&'static str> {
if id == &*self.node {
Some("exchange")
} else if self.depot.as_deref() == Some(id) {
Some("depot")
} else {
None
}
}
pub fn subscribe(&self, filter: TapFilter) -> Result<TapSubscription, TapError> {
self.subscribe_with(filter, None)
}
pub fn subscribe_with(
&self,
filter: TapFilter,
grant: Option<PayloadGrant>,
) -> Result<TapSubscription, TapError> {
let id = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
let subscriber = Arc::new(Subscriber {
id,
filter,
grant,
state: Mutex::new(SubscriberState {
ring: BoundedRing::new(self.shared.capacity.load(Ordering::Relaxed)),
one_in: NonZeroU32::MIN,
seen: 0,
next_seq: 1,
}),
});
let mut subscribers = lock(&self.shared.subscribers, "tap subscribers")?;
subscribers.push(Arc::clone(&subscriber));
self.shared
.active
.store(subscribers.len(), Ordering::Release);
drop(subscribers);
Ok(TapSubscription {
shared: Arc::clone(&self.shared),
subscriber,
})
}
#[must_use]
pub fn observations(&self) -> u64 {
self.shared.observed.load(Ordering::Relaxed)
}
#[cfg(test)]
#[must_use]
pub(crate) fn server_observations(&self) -> u64 {
self.shared.observed_server.load(Ordering::Relaxed)
}
#[must_use]
pub fn subscriber_count(&self) -> usize {
self.shared.active.load(Ordering::Acquire)
}
#[must_use]
pub fn dropped_total(&self) -> u64 {
self.shared.dropped_total.load(Ordering::Relaxed)
}
#[cfg(test)]
pub(crate) fn poison_subscribers(&self) -> bool {
std::thread::scope(|scope| {
scope
.spawn(|| {
let _held = self.shared.subscribers.lock();
std::panic::resume_unwind(Box::new("poisoned for a test"));
})
.join()
.is_err()
}) && self.shared.subscribers.is_poisoned()
}
pub fn name_station(&self, connection: u64, station: &str) -> Result<(), TapError> {
lock(&self.shared.stations, "tap stations")?.insert(connection, Arc::from(station));
Ok(())
}
pub fn forget_station(&self, connection: u64) -> Result<(), TapError> {
lock(&self.shared.stations, "tap stations")?.remove(&connection);
Ok(())
}
pub fn name_receiver(&self, subscriber: u64, connection: u64) -> Result<(), TapError> {
lock(&self.shared.receivers, "tap receivers")?.insert(subscriber, connection);
Ok(())
}
pub fn forget_receiver(&self, subscriber: u64) -> Result<(), TapError> {
lock(&self.shared.receivers, "tap receivers")?.remove(&subscriber);
Ok(())
}
pub fn forget_receivers_of(&self, connection: u64) -> Result<(), TapError> {
lock(&self.shared.receivers, "tap receivers")?.retain(|_, owner| *owner != connection);
Ok(())
}
pub fn lend_recipients(&self) -> Result<Option<PublishRecord>, TapError> {
if self.shared.active.load(Ordering::Acquire) == 0 {
return Ok(None);
}
self.shared.lent.fetch_add(1, Ordering::Relaxed);
Ok(Some(
lock(&self.shared.buffers, "tap buffers")?
.pop()
.unwrap_or_default(),
))
}
pub fn give_back(&self, mut buffer: PublishRecord) -> Result<(), TapError> {
buffer.clear();
lock(&self.shared.buffers, "tap buffers")?.push(buffer);
Ok(())
}
#[must_use]
pub fn recipients_lent(&self) -> u64 {
self.shared.lent.load(Ordering::Relaxed)
}
#[must_use]
pub fn unsubscribe_failures(&self) -> u64 {
self.shared.unsubscribe_failures.load(Ordering::Relaxed)
}
}
#[derive(Debug)]
pub struct TapSubscription {
shared: Arc<TapShared>,
subscriber: Arc<Subscriber>,
}
fn elapsed_ms(elapsed: std::time::Duration) -> Option<u64> {
u64::try_from(elapsed.as_millis()).ok()
}
fn gather<'station>(
recipients: usize,
named: usize,
first_at: impl Fn(usize) -> Option<(&'station Arc<str>, Instant)>,
) -> Result<Option<Arc<[Reached]>>, TapError> {
let Some(first) = (0..recipients).find_map(&first_at) else {
return if named == 0 {
Ok(None)
} else {
Err(TapError::StationsDisagree { named, found: 0 })
};
};
let mut next = 0;
let mut found = 0_usize;
let reached = |(station, queued_at): (&Arc<str>, Instant)| Reached {
station: Arc::clone(station),
queued_at,
};
let list: Arc<[Reached]> = (0..named)
.map(|_| {
match (next..recipients)
.find_map(|index| first_at(index).map(|station| (index, station)))
{
Some((index, station)) => {
next = index + 1;
found += 1;
reached(station)
}
None => reached(first),
}
})
.collect();
let beyond = (next..recipients)
.filter(|index| first_at(*index).is_some())
.count();
if found != named || beyond != 0 {
return Err(TapError::StationsDisagree {
named,
found: found + beyond,
});
}
Ok(Some(list))
}
#[cfg(test)]
mod gather_tests {
use std::sync::Arc;
use std::time::{Duration, Instant};
use super::{Reached, TapError, elapsed_ms, gather};
#[test]
fn an_elapsed_time_beyond_u64_milliseconds_has_no_reading() {
assert_eq!(elapsed_ms(Duration::from_millis(7)), Some(7));
assert_eq!(elapsed_ms(Duration::MAX), None);
}
#[test]
fn a_fill_that_disagrees_with_the_count_is_refused() {
let ledger: Arc<str> = Arc::from("orders.ledger");
let queued = Instant::now();
let gathered = gather(2, 2, |index| (index == 0).then_some((&ledger, queued)));
assert!(
matches!(
gathered,
Err(TapError::StationsDisagree { named: 2, found: 1 })
),
"{gathered:?}"
);
}
#[test]
fn a_fill_that_agrees_builds_the_list() -> Result<(), TapError> {
let ledger: Arc<str> = Arc::from("orders.ledger");
let audit: Arc<str> = Arc::from("orders.audit");
let queued = Instant::now();
let first_at = |index: usize| match index {
0 => Some((&ledger, queued)),
2 => Some((&audit, queued)),
_ => None,
};
let reached = |station: &Arc<str>| Reached {
station: Arc::clone(station),
queued_at: queued,
};
assert_eq!(
gather(3, 2, first_at)?,
Some(Arc::from(vec![reached(&ledger), reached(&audit)]))
);
assert_eq!(gather(3, 0, |_| None)?, None);
Ok(())
}
}