Skip to main content

decdn_node/load_shed/
egress.rs

1//! A lock-free rolling egress meter. Delivery loops call [`EgressMeter::record`]
2//! with each delivered chunk's byte length (the bao-stream content plus
3//! interleaved proof — the same quantity billed as `delivered`, not the full
4//! QUIC frame); a periodic task calls [`EgressMeter::sample`] once per fixed
5//! interval to fold the bytes delivered since the last sample into an
6//! exponentially-weighted bytes/sec rate that the load-shed policy and the
7//! metrics gauge read.
8
9use std::sync::atomic::{AtomicU64, Ordering};
10
11/// Cumulative delivered bytes plus the last computed EWMA rate. All state
12/// is atomic, so `record` stays off any lock on the hot delivery path.
13#[derive(Debug, Default)]
14pub struct EgressMeter {
15    /// Monotonic total delivered bytes.
16    cumulative: AtomicU64,
17    /// `cumulative` as of the last `sample` — the baseline for the next delta.
18    last_sample_cumulative: AtomicU64,
19    /// Whether `sample` has run at least once (seeds the EWMA on the first run).
20    seeded: AtomicU64,
21    /// Last computed bytes/sec EWMA.
22    bps: AtomicU64,
23}
24
25impl EgressMeter {
26    /// An unseeded meter. The first [`Self::sample`] seeds the EWMA rather
27    /// than reporting a rate against an unknown baseline.
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Add `bytes` of delivered content to the running total. Called per chunk
34    /// with the same length billed as `delivered` (bao content plus proof).
35    pub fn record(&self, bytes: u64) {
36        self.cumulative.fetch_add(bytes, Ordering::Relaxed);
37    }
38
39    /// Fold the bytes delivered since the last call into the EWMA and return the
40    /// new bytes/sec. `interval_secs` is the fixed tick period; `0` is a no-op
41    /// that returns the prior rate.
42    pub fn sample(&self, interval_secs: u64) -> u64 {
43        if interval_secs == 0 {
44            return self.bps.load(Ordering::Relaxed);
45        }
46        let now = self.cumulative.load(Ordering::Relaxed);
47        let prev = self.last_sample_cumulative.swap(now, Ordering::Relaxed);
48        let delta = now.saturating_sub(prev);
49        let instant = delta / interval_secs;
50        // Seed on the first sample; afterwards halve toward the instant rate
51        // (EWMA, alpha = 0.5) using integer math to stay off floats.
52        let next = if self.seeded.swap(1, Ordering::Relaxed) == 0 {
53            instant
54        } else {
55            let last = self.bps.load(Ordering::Relaxed);
56            (instant.saturating_add(last)) / 2
57        };
58        self.bps.store(next, Ordering::Relaxed);
59        next
60    }
61
62    /// The most recently computed bytes/sec, read on the admission hot path.
63    #[must_use]
64    pub fn current_bps(&self) -> u64 {
65        self.bps.load(Ordering::Relaxed)
66    }
67}
68
69#[cfg(test)]
70mod tests;