use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use jiff::{SignedDuration, Timestamp};
use tokio::sync::mpsc::error::{TryRecvError, TrySendError};
use tokio::sync::{Notify, mpsc, watch};
use tracing::Instrument as _;
use tollgate_core::{DenyReason, LocalSharding, Locality, UsageEvent};
use tollgate_store::Clock;
use tollgate_store::{MAX_INGEST_BATCH, UsageSink};
#[derive(Debug, Clone, Copy)]
pub struct UsageWriterConfig {
pub queue_capacity: usize,
pub max_batch: usize,
pub flush_interval: std::time::Duration,
pub retry_backoff: std::time::Duration,
pub shutdown_drain_deadline: std::time::Duration,
pub ingest_timeout: std::time::Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UsageWriterConfigError(pub &'static str);
impl std::fmt::Display for UsageWriterConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for UsageWriterConfigError {}
impl UsageWriterConfig {
pub fn validate(&self) -> Result<(), UsageWriterConfigError> {
if self.queue_capacity == 0 {
return Err(UsageWriterConfigError("queue_capacity must be positive"));
}
if self.max_batch > MAX_INGEST_BATCH {
return Err(UsageWriterConfigError(
"max_batch exceeds the ingest endpoint's documented limit",
));
}
if self.max_batch == 0 {
return Err(UsageWriterConfigError("max_batch must be positive"));
}
if self.flush_interval.is_zero() {
return Err(UsageWriterConfigError("flush_interval must be positive"));
}
if self.retry_backoff.is_zero() {
return Err(UsageWriterConfigError("retry_backoff must be positive"));
}
if self.shutdown_drain_deadline.is_zero() {
return Err(UsageWriterConfigError(
"shutdown_drain_deadline must be positive",
));
}
if self.ingest_timeout.is_zero() {
return Err(UsageWriterConfigError("ingest_timeout must be positive"));
}
Ok(())
}
}
#[repr(align(128))]
#[derive(Debug)]
struct Contended(AtomicU64);
impl Contended {
const fn zero() -> Self {
Contended(AtomicU64::new(0))
}
#[inline]
fn bump(&self, by: u64) {
self.0.fetch_add(by, Ordering::Relaxed);
}
#[inline]
fn get(&self) -> u64 {
self.0.load(Ordering::Relaxed)
}
}
#[derive(Debug)]
pub struct WriterCounters {
unattributed: AtomicU64,
attribution_unreported_batches: AtomicU64,
attribution_degraded: AtomicBool,
counter_overflow: AtomicBool,
accepted: AtomicU64,
duplicate: AtomicU64,
rejected: AtomicU64,
lost: AtomicU64,
unresolved: AtomicU64,
last_ingest_ms: AtomicI64,
settled: AtomicU64,
lanes: OnceLock<Arc<[LaneStats]>>,
shed: Contended,
}
impl WriterCounters {
#[must_use]
pub const fn new() -> Self {
WriterCounters {
unattributed: AtomicU64::new(0),
attribution_unreported_batches: AtomicU64::new(0),
attribution_degraded: AtomicBool::new(false),
counter_overflow: AtomicBool::new(false),
accepted: AtomicU64::new(0),
duplicate: AtomicU64::new(0),
rejected: AtomicU64::new(0),
lost: AtomicU64::new(0),
unresolved: AtomicU64::new(0),
last_ingest_ms: AtomicI64::new(i64::MIN),
settled: AtomicU64::new(0),
lanes: OnceLock::new(),
shed: Contended::zero(),
}
}
fn record_ingest(&self, report: &tollgate_store::IngestReport, at: Timestamp) {
self.add_outcome(&self.accepted, report.accepted);
self.add_outcome(&self.duplicate, report.duplicate);
self.add_outcome(&self.rejected, report.rejected);
match report.unattributed {
Some(n) => self.add_outcome(&self.unattributed, n),
None => self.add_outcome(&self.attribution_unreported_batches, 1),
}
let degraded = report.unattributed != Some(0);
let previous = self.attribution_degraded.swap(degraded, Ordering::Relaxed);
if degraded && !previous {
tracing::warn!(coverage_complete = false, unattributed = ?report.unattributed,
"credential activity coverage is incomplete or unavailable");
} else if previous && !degraded {
tracing::info!(
coverage_complete = true,
"credential attribution reporting recovered for this batch"
);
}
self.last_ingest_ms
.store(at.as_millisecond(), Ordering::Relaxed);
}
fn record_lost(&self, events: u64) {
self.add_outcome(&self.lost, events);
}
fn add_outcome(&self, counter: &AtomicU64, delta: u64) {
if counter
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |old| {
old.checked_add(delta)
})
.is_err()
{
counter.store(u64::MAX, Ordering::Relaxed);
if !self.counter_overflow.swap(true, Ordering::Relaxed) {
tracing::error!(
counter_overflow = true,
"usage outcome counter overflow; totals are saturated"
);
}
}
}
fn set_unresolved(&self, permits: u64) {
self.unresolved.store(permits, Ordering::Relaxed);
}
fn settled(&self, events: u64) {
self.settled.fetch_add(events, Ordering::Relaxed);
}
fn unaccounted(&self) -> u64 {
let entered = self.lanes.get().map_or(0, |lanes| {
lanes.iter().fold(0u64, |total, lane| {
total.wrapping_add(lane.enqueued.load(Ordering::Relaxed))
})
});
entered.saturating_sub(self.settled.load(Ordering::Relaxed))
}
fn record_shed(&self) {
self.shed.bump(1);
}
#[must_use]
pub fn stats(&self) -> WriterStats {
WriterStats {
unattributed: self.unattributed.load(Ordering::Relaxed),
attribution_unreported_batches: self
.attribution_unreported_batches
.load(Ordering::Relaxed),
counter_overflow: self.counter_overflow.load(Ordering::Relaxed),
accepted: self.accepted.load(Ordering::Relaxed),
duplicate: self.duplicate.load(Ordering::Relaxed),
rejected: self.rejected.load(Ordering::Relaxed),
lost: self.lost.load(Ordering::Relaxed),
unresolved: self.unresolved.load(Ordering::Relaxed),
}
}
fn last_ingest_at(&self) -> Option<Timestamp> {
let millis = self.last_ingest_ms.load(Ordering::Relaxed);
(millis != i64::MIN)
.then(|| Timestamp::from_millisecond(millis).ok())
.flatten()
}
fn health(&self, queue_depth: usize, queue_capacity: usize) -> WriterHealth {
WriterHealth {
stats: self.stats(),
unaccounted: self.unaccounted(),
shed: self.shed.get(),
queue_depth,
queue_capacity,
last_ingest_at: self.last_ingest_at(),
}
}
}
impl Default for WriterCounters {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriterHealth {
pub stats: WriterStats,
pub unaccounted: u64,
pub shed: u64,
pub queue_depth: usize,
pub queue_capacity: usize,
pub last_ingest_at: Option<Timestamp>,
}
impl WriterHealth {
#[must_use]
pub fn ingest_age(&self, now: Timestamp) -> Option<SignedDuration> {
self.last_ingest_at.map(|at| now.duration_since(at))
}
}
#[repr(align(128))]
#[derive(Debug)]
struct LaneStats {
enqueued: AtomicU64,
rung: AtomicBool,
}
#[derive(Debug)]
struct Queue {
stats: Arc<[LaneStats]>,
doorbell: Notify,
draining: AtomicBool,
ring_at: usize,
counters: Arc<WriterCounters>,
}
#[derive(Debug)]
struct RingOnDrop(Arc<Queue>);
impl Drop for RingOnDrop {
fn drop(&mut self) {
self.0.doorbell.notify_one();
}
}
#[repr(align(128))]
#[derive(Debug)]
struct Lane {
tx: mpsc::Sender<UsageEvent>,
index: usize,
queue: RingOnDrop,
}
#[derive(Clone)]
pub struct UsageRecorder {
lanes: Arc<[Arc<Lane>]>,
layout: LocalSharding,
queue: Arc<Queue>,
}
impl UsageRecorder {
pub fn try_reserve(&self) -> Result<UsagePermit, DenyReason> {
let count = self.lanes.len();
let mut index = Locality::current().index(self.layout);
for _ in 0..count {
let lane = &self.lanes[index];
match lane.tx.clone().try_reserve_owned() {
Ok(permit) => {
return Ok(UsagePermit {
permit: Some(permit),
lane: Arc::clone(lane),
});
}
Err(TrySendError::Full(_)) => {}
Err(TrySendError::Closed(_)) => break,
}
index += 1;
if index == count {
index = 0;
}
}
self.queue.counters.record_shed();
Err(DenyReason::AccountingBackpressure)
}
#[must_use]
pub fn is_closed(&self) -> bool {
self.lanes[0].tx.is_closed()
}
pub(crate) async fn closed(&self) {
self.lanes[0].tx.closed().await;
}
#[must_use]
pub fn health(&self) -> WriterHealth {
let (depth, capacity) = self.lanes.iter().fold((0, 0), |(depth, capacity), lane| {
(
depth + lane.tx.max_capacity() - lane.tx.capacity(),
capacity + lane.tx.max_capacity(),
)
});
self.queue.counters.health(depth, capacity)
}
}
pub struct UsagePermit {
permit: Option<mpsc::OwnedPermit<UsageEvent>>,
lane: Arc<Lane>,
}
impl UsagePermit {
pub fn record(mut self, event: UsageEvent) {
let permit = self
.permit
.take()
.expect("a permit is consumed only by record, which consumes the permit");
let queue = &self.lane.queue.0;
let stats = &queue.stats[self.lane.index];
stats.enqueued.fetch_add(1, Ordering::Relaxed);
let tx = permit.send(event);
if tx.max_capacity() - tx.capacity() >= queue.ring_at
&& !stats.rung.load(Ordering::Relaxed)
&& !stats.rung.swap(true, Ordering::AcqRel)
{
queue.doorbell.notify_one();
}
}
}
impl Drop for UsagePermit {
fn drop(&mut self) {
drop(self.permit.take());
let queue = &self.lane.queue.0;
if queue.draining.load(Ordering::Acquire) {
queue.doorbell.notify_one();
}
}
}
impl tollgate_core::UsageSlot for UsagePermit {
fn record(self, event: UsageEvent) {
UsagePermit::record(self, event);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriterStats {
pub unattributed: u64,
pub attribution_unreported_batches: u64,
pub counter_overflow: bool,
pub accepted: u64,
pub duplicate: u64,
pub rejected: u64,
pub lost: u64,
pub unresolved: u64,
}
impl WriterStats {
pub const ZERO: WriterStats = WriterStats {
unattributed: 0,
attribution_unreported_batches: 0,
counter_overflow: false,
accepted: 0,
duplicate: 0,
rejected: 0,
lost: 0,
unresolved: 0,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WriterShutdownError {
pub unaccounted: u64,
pub panicked: bool,
}
impl std::fmt::Display for WriterShutdownError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cause = if self.panicked { "panicked" } else { "aborted" };
write!(
f,
"usage writer {cause} before reporting; at least {} committed charge(s) have no billing record",
self.unaccounted
)
}
}
impl std::error::Error for WriterShutdownError {}
pub struct UsageWriter {
shutdown: watch::Sender<bool>,
deadline: Arc<crate::ShutdownDeadline>,
handle: Option<tokio::task::JoinHandle<WriterStats>>,
counters: Arc<WriterCounters>,
queue: Arc<[mpsc::WeakSender<UsageEvent>]>,
queue_capacity: usize,
}
const MIN_LANE_CAPACITY: usize = 64;
fn lane_count(capacity: usize) -> usize {
let parallelism = LocalSharding::available_parallelism()
.get()
.next_power_of_two();
let affordable = capacity / MIN_LANE_CAPACITY;
if affordable < 2 {
return 1;
}
parallelism.min(1 << affordable.ilog2())
}
fn lane_capacity(total: usize, count: usize, index: usize) -> usize {
total / count + usize::from(index < total % count)
}
impl UsageWriter {
pub fn spawn(
sink: Arc<dyn UsageSink>,
clock: Arc<dyn Clock>,
config: UsageWriterConfig,
) -> Result<(UsageRecorder, UsageWriter), UsageWriterConfigError> {
config.validate()?;
Ok(Self::spawn_lanes(
sink,
clock,
config,
lane_count(config.queue_capacity),
))
}
fn spawn_lanes(
sink: Arc<dyn UsageSink>,
clock: Arc<dyn Clock>,
config: UsageWriterConfig,
count: usize,
) -> (UsageRecorder, UsageWriter) {
let (senders, receivers): (Vec<_>, Vec<_>) = (0..count)
.map(|index| mpsc::channel(lane_capacity(config.queue_capacity, count, index)))
.unzip();
let weak: Arc<[mpsc::WeakSender<UsageEvent>]> =
senders.iter().map(mpsc::Sender::downgrade).collect();
let (shutdown, shutdown_rx) = watch::channel(false);
let counters = Arc::new(WriterCounters::new());
let stats: Arc<[LaneStats]> = (0..count)
.map(|_| LaneStats {
enqueued: AtomicU64::new(0),
rung: AtomicBool::new(false),
})
.collect();
counters
.lanes
.set(Arc::clone(&stats))
.expect("a fresh counter set has no lanes yet");
let smallest_lane = config.queue_capacity / count;
let queue = Arc::new(Queue {
stats,
doorbell: Notify::new(),
draining: AtomicBool::new(false),
ring_at: config.max_batch.min(smallest_lane / 2).max(1),
counters: Arc::clone(&counters),
});
let lanes: Arc<[Arc<Lane>]> = senders
.into_iter()
.enumerate()
.map(|(index, tx)| {
Arc::new(Lane {
tx,
index,
queue: RingOnDrop(Arc::clone(&queue)),
})
})
.collect();
let deadline = Arc::new(crate::ShutdownDeadline::default());
let handle = tokio::spawn(
run(
Writer {
sink,
clock,
config,
counters: Arc::clone(&counters),
deadline: Arc::clone(&deadline),
},
Lanes {
rx: receivers,
weak: Arc::clone(&weak),
queue: Arc::clone(&queue),
},
shutdown_rx,
)
.instrument(tracing::info_span!(
"usage_writer",
queue_capacity = config.queue_capacity,
lanes = count,
max_batch = config.max_batch
)),
);
(
UsageRecorder {
lanes,
layout: LocalSharding::new(
std::num::NonZeroUsize::new(count).expect("lane_count is at least one"),
),
queue,
},
UsageWriter {
shutdown,
deadline,
handle: Some(handle),
counters,
queue: weak,
queue_capacity: config.queue_capacity,
},
)
}
#[must_use]
pub fn health(&self) -> WriterHealth {
let depth = self
.queue
.iter()
.filter_map(mpsc::WeakSender::upgrade)
.map(|tx| tx.max_capacity() - tx.capacity())
.sum();
self.counters.health(depth, self.queue_capacity)
}
pub async fn shutdown(mut self) -> Result<WriterStats, WriterShutdownError> {
crate::signal(&self.shutdown, true, "usage-writer shutdown");
let Some(handle) = self.handle.as_mut() else {
return Err(self.died(false));
};
match handle.await {
Ok(stats) => Ok(stats),
Err(join) => Err(self.died(join.is_panic())),
}
}
pub(crate) fn stop_at(&self, deadline: tokio::time::Instant) {
self.deadline.constrain(deadline);
crate::signal(&self.shutdown, true, "usage-writer shutdown");
}
fn died(&self, panicked: bool) -> WriterShutdownError {
WriterShutdownError {
unaccounted: self.counters.unaccounted(),
panicked,
}
}
}
impl Drop for UsageWriter {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
handle.abort();
}
}
}
struct Writer {
sink: Arc<dyn UsageSink>,
clock: Arc<dyn Clock>,
config: UsageWriterConfig,
counters: Arc<WriterCounters>,
deadline: Arc<crate::ShutdownDeadline>,
}
impl Writer {
fn account_for(&self, batch: &[UsageEvent]) {
self.counters.settled(batch.len() as u64);
}
}
struct Lanes {
rx: Vec<mpsc::Receiver<UsageEvent>>,
weak: Arc<[mpsc::WeakSender<UsageEvent>]>,
queue: Arc<Queue>,
}
enum Collected {
Full,
Empty { disconnected: bool },
}
impl Lanes {
fn collect(&mut self, batch: &mut Vec<UsageEvent>, max_batch: usize) -> Collected {
let mut disconnected = true;
for (index, rx) in self.rx.iter_mut().enumerate() {
self.queue.stats[index].rung.store(false, Ordering::Release);
loop {
if batch.len() >= max_batch {
return Collected::Full;
}
match rx.try_recv() {
Ok(event) => batch.push(event),
Err(TryRecvError::Empty) => {
disconnected = false;
break;
}
Err(TryRecvError::Disconnected) => break,
}
}
}
Collected::Empty { disconnected }
}
}
async fn run(writer: Writer, mut lanes: Lanes, mut shutdown: watch::Receiver<bool>) -> WriterStats {
let config = writer.config;
let max_batch = config.max_batch;
let mut batch: Vec<UsageEvent> = Vec::with_capacity(max_batch);
loop {
if *shutdown.borrow() {
return final_flush(&writer, &mut lanes, &mut batch).await;
}
let deadline = tokio::time::sleep(config.flush_interval);
tokio::pin!(deadline);
let stop = loop {
let due = tokio::select! {
() = lanes.queue.doorbell.notified() => false,
() = &mut deadline => true,
changed = shutdown.changed() => {
if changed.is_err() || *shutdown.borrow() {
break true;
}
continue;
}
};
let disconnected = loop {
match lanes.collect(&mut batch, max_batch) {
Collected::Full => {
flush_retrying(&writer, &mut batch, &mut shutdown).await;
if *shutdown.borrow() || shutdown.has_changed().is_err() {
break false;
}
}
Collected::Empty { disconnected } => break disconnected,
}
};
if *shutdown.borrow() || shutdown.has_changed().is_err() {
break true;
}
if due && !batch.is_empty() {
flush_retrying(&writer, &mut batch, &mut shutdown).await;
}
if disconnected {
break true;
}
if due {
break false;
}
};
if stop {
return final_flush(&writer, &mut lanes, &mut batch).await;
}
}
}
async fn flush_retrying(
writer: &Writer,
batch: &mut Vec<UsageEvent>,
shutdown: &mut watch::Receiver<bool>,
) {
let Writer {
sink,
clock,
config,
counters,
..
} = writer;
let mut outage: Option<(tokio::time::Instant, u64)> = None;
loop {
let now = clock.now();
let ingest =
tokio::time::timeout(config.ingest_timeout, ingest_checked(&**sink, batch, now));
let outcome = tokio::select! {
outcome = ingest => outcome,
_ = shutdown.changed() => return,
};
match outcome {
Ok(Ok(report)) => {
if let Some((began, attempts)) = outage {
tracing::info!(
attempts,
outage_ms = began.elapsed().as_millis(),
"usage sink recovered"
);
}
counters.record_ingest(&report, now);
writer.account_for(batch);
batch.clear();
return;
}
Ok(Err(refused)) if !refused.is_retryable() => {
counters.record_lost(batch.len() as u64);
tracing::error!(
events = batch.len(),
%refused,
"usage sink refused this batch and will refuse it again; \
counted lost so later events are not blocked behind it"
);
writer.account_for(batch);
batch.clear();
return;
}
outcome => {
let timed_out = outcome.is_err();
let attempts = match &mut outage {
Some((_, attempts)) => {
*attempts += 1;
*attempts
}
none => {
*none = Some((tokio::time::Instant::now(), 1));
tracing::warn!(
events = batch.len(),
timed_out,
"usage sink failing; batching up and retrying"
);
1
}
};
if attempts > 1 {
tracing::debug!(attempts, timed_out, "usage sink still failing");
}
tokio::select! {
_ = tokio::time::sleep(config.retry_backoff) => {}
_ = shutdown.changed() => {}
}
if *shutdown.borrow() || shutdown.has_changed().is_err() {
if let Some((began, attempts)) = outage {
tracing::warn!(
attempts,
outage_ms = began.elapsed().as_millis(),
events = batch.len(),
"shutdown observed during a sink outage; \
the final flush decides these events' fate"
);
}
return;
}
}
}
}
}
async fn final_flush(
writer: &Writer,
lanes: &mut Lanes,
batch: &mut Vec<UsageEvent>,
) -> WriterStats {
let config = &writer.config;
let max_batch = config.max_batch;
lanes.queue.draining.store(true, Ordering::Release);
for rx in &mut lanes.rx {
rx.close();
}
let deadline = writer.deadline.within(config.shutdown_drain_deadline);
let mut expired = false;
loop {
let drained = loop {
match lanes.collect(batch, max_batch) {
Collected::Full => flush_bounded(writer, batch, deadline).await,
Collected::Empty { disconnected } => break disconnected,
}
};
if drained || expired {
if !batch.is_empty() {
flush_bounded(writer, batch, deadline).await;
}
if !drained {
writer
.counters
.set_unresolved(outstanding_permits(&lanes.weak));
}
return writer.counters.stats();
}
if tokio::time::timeout_at(deadline, lanes.queue.doorbell.notified())
.await
.is_err()
{
expired = true;
}
}
}
async fn flush_bounded(
writer: &Writer,
batch: &mut Vec<UsageEvent>,
deadline: tokio::time::Instant,
) {
let Writer {
sink,
clock,
config,
counters,
..
} = writer;
const FINAL_FLUSH_ATTEMPTS: u32 = 3;
let mut delivered = false;
for attempt in 1..=FINAL_FLUSH_ATTEMPTS {
let attempt_deadline = deadline.min(tokio::time::Instant::now() + config.ingest_timeout);
let now = clock.now();
match tokio::time::timeout_at(attempt_deadline, ingest_checked(&**sink, batch, now)).await {
Ok(Ok(report)) => {
counters.record_ingest(&report, now);
delivered = true;
break;
}
Ok(Err(_)) if attempt < FINAL_FLUSH_ATTEMPTS => {
tokio::time::sleep_until(
deadline.min(tokio::time::Instant::now() + config.retry_backoff),
)
.await;
if tokio::time::Instant::now() >= deadline {
break;
}
}
Err(_) => break,
Ok(Err(_)) => {}
}
}
if !delivered {
counters.record_lost(batch.len() as u64);
}
writer.account_for(batch);
batch.clear();
}
async fn ingest_checked(
sink: &dyn UsageSink,
events: &[UsageEvent],
now: Timestamp,
) -> Result<tollgate_store::IngestReport, tollgate_store::IngestError> {
let report = sink.ingest(events, now).await?;
report
.validate(events.len())
.map_err(tollgate_store::IngestError::Unavailable)?;
Ok(report)
}
fn outstanding_permits(lanes: &[mpsc::WeakSender<UsageEvent>]) -> u64 {
lanes
.iter()
.filter_map(mpsc::WeakSender::upgrade)
.map(|tx| (tx.max_capacity() - tx.capacity()) as u64)
.sum()
}
#[cfg(test)]
mod layout_tests {
#[test]
fn attribution_and_existing_outcome_counters_saturate_visibly() {
use super::*;
use tracing_subscriber::layer::SubscriberExt;
#[derive(Clone)]
struct OverflowEvents(Arc<AtomicU64>);
impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for OverflowEvents {
fn on_event(
&self,
event: &tracing::Event<'_>,
_: tracing_subscriber::layer::Context<'_, S>,
) {
struct Fields(bool);
impl tracing::field::Visit for Fields {
fn record_debug(&mut self, _: &tracing::field::Field, _: &dyn std::fmt::Debug) {
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
if field.name() == "counter_overflow" {
self.0 = value;
}
}
}
let mut fields = Fields(false);
event.record(&mut fields);
if fields.0 {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
}
let events = Arc::new(AtomicU64::new(0));
tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(OverflowEvents(events.clone())),
)
.unwrap();
let counters = WriterCounters::new();
for counter in [
&counters.accepted,
&counters.duplicate,
&counters.rejected,
&counters.lost,
&counters.unattributed,
&counters.attribution_unreported_batches,
] {
counter.store(u64::MAX - 1, Ordering::Relaxed);
counters.add_outcome(counter, 1);
assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
counters.add_outcome(counter, 1);
assert_eq!(counter.load(Ordering::Relaxed), u64::MAX);
}
assert!(counters.stats().counter_overflow);
assert_eq!(events.load(Ordering::Relaxed), 1);
let counters = WriterCounters::new();
counters.record_ingest(
&tollgate_store::IngestReport {
accepted: 4,
duplicate: 2,
rejected: 1,
unattributed: Some(3),
},
Timestamp::UNIX_EPOCH,
);
counters.record_ingest(
&tollgate_store::IngestReport::default(),
Timestamp::UNIX_EPOCH,
);
let stats = counters.stats();
assert_eq!(
(
stats.accepted,
stats.duplicate,
stats.rejected,
stats.unattributed,
stats.attribution_unreported_batches
),
(4, 2, 1, 3, 1)
);
assert!(!stats.counter_overflow);
}
use super::{Contended, UsageWriter, UsageWriterConfig};
use jiff::Timestamp;
use std::sync::Arc;
#[tokio::test(start_paused = true)]
async fn a_runtime_stop_closes_the_queue_and_bounds_the_actual_drain() {
let (recorder, writer) = UsageWriter::spawn(
tollgate_store::MemoryStore::new(tollgate_store::GrantPolicy::default()).unwrap(),
Arc::new(crate::ManualClock::new(
Timestamp::from_second(100).unwrap(),
)),
UsageWriterConfig {
queue_capacity: 1,
max_batch: 1,
flush_interval: std::time::Duration::from_millis(1),
retry_backoff: std::time::Duration::from_millis(1),
shutdown_drain_deadline: std::time::Duration::from_millis(100),
ingest_timeout: std::time::Duration::from_millis(1),
},
)
.unwrap();
let permit = recorder.try_reserve().unwrap();
let began = tokio::time::Instant::now();
writer.stop_at(began + std::time::Duration::from_millis(5));
tokio::time::timeout(std::time::Duration::from_millis(1), recorder.closed())
.await
.unwrap();
let stats = writer.shutdown().await.unwrap();
assert_eq!(stats.unresolved, 1);
assert_eq!(began.elapsed(), std::time::Duration::from_millis(5));
drop(permit);
}
#[test]
fn request_path_counters_are_isolated_on_supported_cache_lines() {
assert_eq!(align_of::<Contended>(), 128);
assert_eq!(size_of::<Contended>(), 128);
}
}
#[cfg(test)]
mod lane_tests {
use super::*;
use std::sync::Mutex;
use std::time::Duration;
use tollgate_core::{AccountId, CostUnits, PolicyRevision, RequestId, UsageSource};
use tollgate_store::{IngestError, IngestReport};
#[derive(Default)]
struct Recording(Mutex<Vec<u128>>);
#[async_trait::async_trait]
impl UsageSink for Recording {
async fn ingest(
&self,
events: &[UsageEvent],
_now: Timestamp,
) -> Result<IngestReport, IngestError> {
self.0
.lock()
.unwrap()
.extend(events.iter().map(|event| event.request_id.0));
Ok(IngestReport {
accepted: events.len() as u64,
unattributed: Some(0),
..IngestReport::default()
})
}
}
fn event(id: u128) -> UsageEvent {
UsageEvent::new(
RequestId(id),
AccountId(1),
UsageSource::Overage,
CostUnits(1),
Timestamp::from_second(100).unwrap(),
PolicyRevision::UNSTATED,
None,
)
}
fn config(queue_capacity: usize, max_batch: usize) -> UsageWriterConfig {
UsageWriterConfig {
queue_capacity,
max_batch,
flush_interval: Duration::from_secs(60),
retry_backoff: Duration::from_millis(10),
shutdown_drain_deadline: Duration::from_secs(5),
ingest_timeout: Duration::from_secs(5),
}
}
fn spawn(
sink: &Arc<Recording>,
queue_capacity: usize,
max_batch: usize,
lanes: usize,
) -> (UsageRecorder, UsageWriter) {
UsageWriter::spawn_lanes(
Arc::clone(sink) as Arc<dyn UsageSink>,
Arc::new(crate::ManualClock::new(
Timestamp::from_second(100).unwrap(),
)),
config(queue_capacity, max_batch),
lanes,
)
}
#[test]
fn lanes_partition_the_capacity_exactly_and_never_go_below_their_floor() {
for (total, count) in [(256, 4), (4_096, 16), (100, 3), (7, 7)] {
let sizes: Vec<usize> = (0..count).map(|i| lane_capacity(total, count, i)).collect();
assert_eq!(sizes.iter().sum::<usize>(), total, "{total}/{count}");
assert!(sizes.iter().max().unwrap() - sizes.iter().min().unwrap() <= 1);
}
assert_eq!(lane_count(1), 1);
assert_eq!(
lane_count(MIN_LANE_CAPACITY * 2 - 1),
1,
"one lane below two floors"
);
for capacity in [128, 4_096, 65_536] {
let count = lane_count(capacity);
assert!(count.is_power_of_two());
assert!(
capacity / count >= MIN_LANE_CAPACITY,
"{capacity} -> {count}"
);
}
}
#[tokio::test(start_paused = true)]
async fn the_shed_point_is_the_whole_queue_across_lanes() {
let sink = Arc::new(Recording::default());
let (recorder, writer) = spawn(&sink, 256, 64, 4);
let permits: Vec<_> = (0..256).map(|_| recorder.try_reserve().unwrap()).collect();
assert_eq!(recorder.health().queue_depth, 256);
assert_eq!(recorder.health().queue_capacity, 256);
assert_eq!(
recorder.try_reserve().err(),
Some(DenyReason::AccountingBackpressure)
);
assert_eq!(recorder.health().shed, 1);
drop(permits);
assert_eq!(recorder.health().queue_depth, 0);
assert!(writer.shutdown().await.unwrap().unresolved == 0);
}
#[tokio::test(start_paused = true)]
async fn the_drain_delivers_every_lane_and_reports_every_lanes_permits() {
let sink = Arc::new(Recording::default());
let (recorder, writer) = spawn(&sink, 256, 256, 4);
for id in 0..200 {
recorder.try_reserve().unwrap().record(event(id));
}
assert_eq!(recorder.health().unaccounted, 200);
let held: Vec<_> = (0..40).map(|_| recorder.try_reserve().unwrap()).collect();
let stats = writer.shutdown().await.unwrap();
let mut delivered = sink.0.lock().unwrap().clone();
delivered.sort_unstable();
assert_eq!(
delivered,
(0..200).collect::<Vec<_>>(),
"every lane drained"
);
assert_eq!(stats.accepted, 200);
assert_eq!(
stats.unresolved, 40,
"permits held across lanes are all reported"
);
assert_eq!(recorder.health().unaccounted, 0);
drop(held);
}
#[tokio::test(start_paused = true)]
async fn a_resolving_permit_in_any_lane_completes_the_drain() {
let sink = Arc::new(Recording::default());
let (recorder, writer) = spawn(&sink, 256, 64, 4);
let held: Vec<_> = (0..100).map(|_| recorder.try_reserve().unwrap()).collect();
let began = tokio::time::Instant::now();
let release = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(50)).await;
for (id, permit) in (0..).zip(held) {
permit.record(event(id));
}
});
let stats = writer.shutdown().await.unwrap();
release.await.unwrap();
assert_eq!(stats.accepted, 100);
assert_eq!(stats.unresolved, 0);
assert_eq!(
began.elapsed(),
Duration::from_millis(50),
"woken, not timed out"
);
}
#[tokio::test(start_paused = true)]
async fn a_full_batch_is_delivered_before_the_tick() {
let sink = Arc::new(Recording::default());
let (recorder, writer) = spawn(&sink, 256, 16, 4);
for id in 0..16 {
recorder.try_reserve().unwrap().record(event(id));
}
for _ in 0..10 {
tokio::task::yield_now().await;
}
assert_eq!(sink.0.lock().unwrap().len(), 16, "delivered without a tick");
assert_eq!(writer.shutdown().await.unwrap().accepted, 16);
}
#[tokio::test(start_paused = true)]
async fn a_partial_batch_waits_for_the_tick() {
let sink = Arc::new(Recording::default());
let (recorder, writer) = spawn(&sink, 256, 64, 4);
for id in 0..3 {
recorder.try_reserve().unwrap().record(event(id));
}
for _ in 0..10 {
tokio::task::yield_now().await;
}
assert!(sink.0.lock().unwrap().is_empty(), "no wake per event");
tokio::time::sleep(Duration::from_secs(61)).await;
assert_eq!(sink.0.lock().unwrap().len(), 3, "the tick delivered it");
assert_eq!(writer.shutdown().await.unwrap().accepted, 3);
}
#[tokio::test(start_paused = true)]
async fn dropping_the_recorder_stops_the_writer_promptly() {
let sink = Arc::new(Recording::default());
let (recorder, mut writer) = spawn(&sink, 256, 64, 4);
recorder.try_reserve().unwrap().record(event(1));
drop(recorder);
let handle = writer.handle.take().unwrap();
let stats = tokio::time::timeout(Duration::from_millis(1), handle)
.await
.expect("stopped before any tick")
.unwrap();
assert_eq!(stats.accepted, 1);
}
}