Skip to main content

bacnet_ops/limits/
account.rs

1//! What one caller's requests cost, and the caller's own rate.
2//!
3//! A front end serving several callers (the MCP server's principals, #2066)
4//! runs each call under an [`Account`] with [`charged`]. Every request the
5//! call sends through [`RequestLimits`](super::RequestLimits) then counts
6//! against it, with the time it waited for the limits, and first takes a
7//! token from the caller's [`TokenBucket`], when the account has one. The
8//! account rides in a task-local, so the engine's read paths charge it
9//! without passing it along; a request made outside [`charged`] (the TUI's)
10//! charges nothing.
11
12use std::future::Future;
13use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
14use std::sync::{Arc, Mutex, PoisonError};
15use std::time::Duration;
16
17use tokio::time::Instant;
18
19tokio::task_local! {
20    static ACCOUNT: Arc<Account>;
21}
22
23/// One caller's tally: the requests it sent and how long they waited for
24/// the limits, and the caller's token bucket, if it has one.
25#[derive(Debug, Default)]
26pub struct Account {
27    requests: AtomicU32,
28    throttled_us: AtomicU64,
29    bucket: Option<Arc<TokenBucket>>,
30}
31
32impl Account {
33    /// An empty tally, whose requests also take tokens from `bucket`.
34    pub fn new(bucket: Option<Arc<TokenBucket>>) -> Self {
35        Self {
36            requests: AtomicU32::new(0),
37            throttled_us: AtomicU64::new(0),
38            bucket,
39        }
40    }
41
42    /// Requests counted so far.
43    pub fn requests(&self) -> u32 {
44        self.requests.load(Ordering::Relaxed)
45    }
46
47    /// Time the requests spent waiting for a device's gate, the caller's
48    /// bucket, a network's rate or the global rate.
49    pub fn throttled(&self) -> Duration {
50        Duration::from_micros(self.throttled_us.load(Ordering::Relaxed))
51    }
52
53    pub(super) fn count_request(&self) {
54        self.requests.fetch_add(1, Ordering::Relaxed);
55    }
56
57    /// Take back a request counted for a send that never left.
58    fn uncount_request(&self) {
59        // The refunded request was counted just before, by this call's
60        // own task, so the count is above zero here.
61        if self.requests.load(Ordering::Relaxed) > 0 {
62            self.requests.fetch_sub(1, Ordering::Relaxed);
63        }
64    }
65
66    pub(super) fn add_wait(&self, waited: Duration) {
67        let micros = u64::try_from(waited.as_micros()).unwrap_or(u64::MAX);
68        self.throttled_us.fetch_add(micros, Ordering::Relaxed);
69    }
70
71    pub(super) fn bucket(&self) -> Option<&TokenBucket> {
72        self.bucket.as_deref()
73    }
74}
75
76/// Run `future` with every request it sends charged to `account`.
77pub async fn charged<F: Future>(account: Arc<Account>, future: F) -> F::Output {
78    ACCOUNT.scope(account, future).await
79}
80
81/// Take back the last request this task's call was charged for, when its
82/// send failed before anything left (the transport refused it).
83pub fn refund() {
84    if let Some(account) = current() {
85        account.uncount_request();
86    }
87}
88
89/// The account of the call running on this task, if any.
90pub(super) fn current() -> Option<Arc<Account>> {
91    ACCOUNT.try_with(Arc::clone).ok()
92}
93
94/// A token bucket: at most `burst` requests at once, refilled at
95/// `per_second`. A caller that finds it empty waits for its token, which
96/// is booked as it starts waiting, so callers are served in turn and a
97/// dropped wait still spends its token.
98#[derive(Debug)]
99pub struct TokenBucket {
100    per_second: f64,
101    burst: f64,
102    state: Mutex<Tokens>,
103}
104
105#[derive(Debug)]
106struct Tokens {
107    /// Tokens left; below zero, tokens already promised to waiters.
108    left: f64,
109    at: Instant,
110}
111
112impl TokenBucket {
113    /// A bucket of `burst` tokens (at least one), refilled at `per_second`;
114    /// 0 means no limit.
115    pub fn new(per_second: u32, burst: u32) -> Self {
116        let burst = f64::from(burst.max(1));
117        Self {
118            per_second: f64::from(per_second),
119            burst,
120            state: Mutex::new(Tokens {
121                left: burst,
122                at: Instant::now(),
123            }),
124        }
125    }
126
127    /// Take a token, waiting for one when the bucket is empty.
128    pub async fn take(&self) {
129        if self.per_second == 0.0 {
130            return;
131        }
132        let wait = {
133            let mut tokens = self.state.lock().unwrap_or_else(PoisonError::into_inner);
134            let now = Instant::now();
135            let refill = now.saturating_duration_since(tokens.at).as_secs_f64() * self.per_second;
136            tokens.left = (tokens.left + refill).min(self.burst) - 1.0;
137            tokens.at = now;
138            (tokens.left < 0.0).then(|| Duration::from_secs_f64(-tokens.left / self.per_second))
139        };
140        if let Some(wait) = wait {
141            tokio::time::sleep(wait).await;
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    /// A full bucket gives its burst at once, then one token every
151    /// `1 / per_second`; after an idle spell it is full again.
152    #[tokio::test(start_paused = true)]
153    async fn a_bucket_gives_its_burst_then_its_rate() {
154        let bucket = TokenBucket::new(5, 3);
155        let start = Instant::now();
156        let mut times = Vec::new();
157        for _ in 0..5 {
158            bucket.take().await;
159            times.push((Instant::now() - start).as_millis());
160        }
161        assert_eq!(times, [0, 0, 0, 200, 400]);
162        tokio::time::sleep(Duration::from_secs(10)).await;
163        let before = Instant::now();
164        for _ in 0..3 {
165            bucket.take().await;
166        }
167        assert_eq!(Instant::now(), before, "refilled to the burst, no more");
168        bucket.take().await;
169        assert_eq!(Instant::now() - before, Duration::from_millis(200));
170        let unlimited = TokenBucket::new(0, 1);
171        let before = Instant::now();
172        for _ in 0..100 {
173            unlimited.take().await;
174        }
175        assert_eq!(Instant::now(), before);
176    }
177
178    /// A charged future sees its account; one outside sees none.
179    #[tokio::test]
180    async fn the_account_rides_with_the_call() {
181        assert!(current().is_none());
182        let account = Arc::new(Account::new(None));
183        charged(Arc::clone(&account), async {
184            current().expect("an account").count_request();
185        })
186        .await;
187        assert_eq!(account.requests(), 1);
188    }
189}