Skip to main content

bacnet_ops/limits/
mod.rs

1//! The session's request limits.
2//!
3//! [`RequestLimits`] is shared by every worker that asks devices anything:
4//! a global cap on how many requests a second leave across every device,
5//! and one gate per device, so a device never has more than one of our
6//! confirmed requests outstanding, whether the browser, the watch list, a
7//! write or a managed COV renewal sent it (#1791). Small devices often
8//! handle one transaction at a time.
9//!
10//! The MCP server adds two layers (#2066): a rate per network, since a slow
11//! MS/TP trunk behind a router takes far fewer requests than the IP side
12//! ([`RequestLimits::with_network_rates`]), and a per-caller [`Account`]
13//! with its own [`TokenBucket`] ([`charged`]).
14
15mod account;
16
17pub use self::account::{Account, TokenBucket, charged, refund};
18
19use std::collections::{HashMap, VecDeque};
20use std::sync::{Arc, Mutex, PoisonError};
21use std::time::Duration;
22
23use tokio::sync::{OwnedSemaphorePermit, Semaphore};
24use tokio::time::Instant;
25
26/// How long, in seconds, the request-rate meter ([`RequestLimits::recent`])
27/// remembers a request.
28pub const METER_WINDOW_SECS: u64 = 10;
29
30/// Spaces requests evenly: at most `per_second` a second across every job,
31/// the browser's and the watch list's alike. Each caller takes the next free
32/// slot and sleeps until it; the lock is held only to take the slot, never
33/// across the sleep. It also remembers when recent requests went, for the
34/// request-rate meter.
35///
36/// It also holds each device's gate: a one-permit semaphore that a request
37/// to the device holds from before its slot until its answer (see
38/// [`turn`](Self::turn)). The std mutex around the map is held only to look
39/// a gate up, never across an await.
40pub struct RequestLimits {
41    interval: Duration,
42    state: Mutex<Slots>,
43    gates: Mutex<HashMap<u32, Arc<Semaphore>>>,
44    networks: Option<Networks>,
45}
46
47/// The rate of each network: requests to it are spaced as the global cap
48/// spaces all of them. The lock is held only to take a slot.
49struct Networks {
50    /// The spacing of a network without a rate of its own; zero for none.
51    default: Duration,
52    rates: HashMap<u16, Duration>,
53    next: Mutex<HashMap<u16, Instant>>,
54}
55
56impl Networks {
57    /// The instant a request to `network` may go, reserving it; `None`
58    /// when the network has no rate.
59    fn reserve(&self, network: u16, now: Instant) -> Option<Instant> {
60        let interval = self.rates.get(&network).copied().unwrap_or(self.default);
61        if interval.is_zero() {
62            return None;
63        }
64        let mut next = self.next.lock().unwrap_or_else(PoisonError::into_inner);
65        let at = next.entry(network).or_insert(now);
66        let slot = (*at).max(now);
67        *at = slot + interval;
68        Some(slot)
69    }
70}
71
72/// The spacing of `per_second` requests a second; zero for 0 (no cap).
73fn spacing(per_second: u32) -> Duration {
74    match per_second {
75        0 => Duration::ZERO,
76        n => Duration::from_secs(1) / n,
77    }
78}
79
80/// A request's turn at its device: the device's gate, held until this is
81/// dropped, on every path out of the request (an answer, an error, or the
82/// request's future dropped).
83#[must_use = "the device's gate is held only while the turn lives"]
84pub struct Turn {
85    _permit: Option<OwnedSemaphorePermit>,
86}
87
88#[derive(Default)]
89struct Slots {
90    /// The next free slot.
91    next: Option<Instant>,
92    /// The slots handed out within the meter's window, oldest first.
93    recent: VecDeque<Instant>,
94}
95
96/// How long the meter remembers a request.
97const METER_WINDOW: Duration = Duration::from_secs(METER_WINDOW_SECS);
98
99impl RequestLimits {
100    /// A cap of `per_second` requests a second; 0 means no cap.
101    pub fn new(per_second: u32) -> Self {
102        Self {
103            interval: spacing(per_second),
104            state: Mutex::new(Slots::default()),
105            gates: Mutex::new(HashMap::new()),
106            networks: None,
107        }
108    }
109
110    /// These limits, with a rate per network as well: `default` requests a
111    /// second for any network (0 for no cap of its own), and `rates` for
112    /// the networks named, network 0 being the local one. The network
113    /// number doesn't say what link lies behind a router, so a slow MS/TP
114    /// trunk has to be named.
115    pub fn with_network_rates(
116        mut self,
117        default: u32,
118        rates: impl IntoIterator<Item = (u16, u32)>,
119    ) -> Self {
120        self.networks = Some(Networks {
121            default: spacing(default),
122            rates: rates
123                .into_iter()
124                .map(|(network, per_second)| (network, spacing(per_second)))
125                .collect(),
126            next: Mutex::new(HashMap::new()),
127        });
128        self
129    }
130
131    /// True when requests are spaced per network too, so a request has to
132    /// say which network its device is on ([`turn_on`](Self::turn_on)).
133    pub fn paces_networks(&self) -> bool {
134        self.networks.is_some()
135    }
136
137    /// `device`'s gate: one permit, held by whichever request to it is
138    /// outstanding. A managed COV subscription takes it for its renewals.
139    pub fn gate(&self, device: u32) -> Arc<Semaphore> {
140        let mut gates = self.gates.lock().unwrap_or_else(PoisonError::into_inner);
141        Arc::clone(
142            gates
143                .entry(device)
144                .or_insert_with(|| Arc::new(Semaphore::new(1))),
145        )
146    }
147
148    /// Wait for `device`'s gate, then for this request's slot. The turn
149    /// holds the gate until it is dropped, so keep it for exactly the one
150    /// request: a job that takes a second turn while holding the first
151    /// waits on itself.
152    ///
153    /// The gate comes first so a request queued behind another one to the
154    /// same device holds no slot meanwhile. The wait is cancel-safe: a
155    /// dropped wait leaves the semaphore's queue.
156    pub async fn turn(&self, device: u32) -> Turn {
157        self.turn_on(device, None).await
158    }
159
160    /// [`turn`](Self::turn) for a device on `network` (0 for the local
161    /// one; `None` when unknown, which skips the network's rate). The
162    /// caller's token, when its [`Account`] has a bucket, comes first, so
163    /// a caller waiting for its own rate doesn't hold the device's gate.
164    pub async fn turn_on(&self, device: u32, network: Option<u16>) -> Turn {
165        let account = account::current();
166        let start = Instant::now();
167        if let Some(bucket) = account.as_deref().and_then(Account::bucket) {
168            bucket.take().await;
169        }
170        // The gate is never closed, so the permit always comes.
171        let permit = self.gate(device).acquire_owned().await.ok();
172        self.pace(network).await;
173        if let Some(account) = &account {
174            account.count_request();
175            account.add_wait(start.elapsed());
176        }
177        Turn { _permit: permit }
178    }
179
180    /// A slot for one more request while the caller already holds its
181    /// device's turn, as a log page's later requests do: the caller's
182    /// token, then `network`'s rate and the global rate, charged as a
183    /// request of its own.
184    pub async fn slot(&self, network: Option<u16>) {
185        let account = account::current();
186        let start = Instant::now();
187        if let Some(bucket) = account.as_deref().and_then(Account::bucket) {
188            bucket.take().await;
189        }
190        self.pace(network).await;
191        if let Some(account) = &account {
192            account.count_request();
193            account.add_wait(start.elapsed());
194        }
195    }
196
197    /// Wait for `network`'s slot, then the global one.
198    async fn pace(&self, network: Option<u16>) {
199        if let (Some(networks), Some(network)) = (&self.networks, network)
200            && let Some(slot) = networks.reserve(network, Instant::now())
201        {
202            tokio::time::sleep_until(slot).await;
203        }
204        self.wait_slot().await;
205    }
206
207    /// Wait until `device`'s gate is free and a request could go at once,
208    /// taking neither; see [`wait_free`](Self::wait_free) for why.
209    pub async fn wait_device_free(&self, device: u32) {
210        drop(self.gate(device).acquire_owned().await);
211        self.wait_free().await;
212    }
213
214    /// The instant this request may go, reserving it.
215    fn reserve(&self, now: Instant) -> Instant {
216        let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
217        let slot = state.next.map_or(now, |next| next.max(now));
218        state.next = Some(slot + self.interval);
219        while state
220            .recent
221            .front()
222            .is_some_and(|at| now.saturating_duration_since(*at) >= METER_WINDOW)
223        {
224            state.recent.pop_front();
225        }
226        state.recent.push_back(slot);
227        slot
228    }
229
230    /// Wait for this request's slot under the rate cap alone; a request to
231    /// a device takes a [`turn`](Self::turn), which includes one. The call's
232    /// [`Account`], if any, is charged as by [`slot`](Self::slot).
233    pub async fn acquire(&self) {
234        self.slot(None).await;
235    }
236
237    async fn wait_slot(&self) {
238        let slot = self.reserve(Instant::now());
239        if !self.interval.is_zero() {
240            tokio::time::sleep_until(slot).await;
241        }
242    }
243
244    /// Wait until a request could go at once, without reserving a slot: a
245    /// caller that decides only then what to send (the name reads) spends
246    /// no slot on work withdrawn while it waited.
247    ///
248    /// That holds only while the caller doesn't await between this and its
249    /// [`acquire`](Self::acquire): deciding what to send must be
250    /// synchronous (the TUI's name reads take their batch under a std
251    /// mutex). An await there lets other jobs book the free slot first, and
252    /// the caller's request then waits for a later one, for rows that may
253    /// have been withdrawn meanwhile.
254    pub async fn wait_free(&self) {
255        loop {
256            let next = self
257                .state
258                .lock()
259                .unwrap_or_else(PoisonError::into_inner)
260                .next;
261            match next {
262                Some(at) if at > Instant::now() => tokio::time::sleep_until(at).await,
263                _ => return,
264            }
265        }
266    }
267
268    /// Requests whose slot fell in the meter's window up to now.
269    pub fn recent(&self) -> usize {
270        let now = Instant::now();
271        let state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
272        state
273            .recent
274            .iter()
275            .filter(|at| **at <= now && now.saturating_duration_since(**at) < METER_WINDOW)
276            .count()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    /// Requests are spaced `1 / per_second` apart, whatever the caller,
285    /// and a cap of zero never waits.
286    #[tokio::test(start_paused = true)]
287    async fn the_rate_cap_spaces_requests_evenly() {
288        let cap = RequestLimits::new(4);
289        let start = Instant::now();
290        let mut slots = Vec::new();
291        for _ in 0..3 {
292            cap.acquire().await;
293            slots.push(Instant::now() - start);
294        }
295        assert_eq!(
296            slots,
297            [
298                Duration::ZERO,
299                Duration::from_millis(250),
300                Duration::from_millis(500)
301            ]
302        );
303        // After an idle second the next request goes at once.
304        tokio::time::sleep(Duration::from_secs(1)).await;
305        let before = Instant::now();
306        cap.acquire().await;
307        assert_eq!(Instant::now(), before);
308        let none = RequestLimits::new(0);
309        let before = Instant::now();
310        for _ in 0..10 {
311            none.acquire().await;
312        }
313        assert_eq!(Instant::now(), before);
314    }
315
316    /// Waiting for a free slot reserves none: the next request still goes
317    /// as soon as it would have.
318    #[tokio::test(start_paused = true)]
319    async fn waiting_for_a_free_slot_takes_none() {
320        let cap = RequestLimits::new(4);
321        let start = Instant::now();
322        cap.acquire().await;
323        cap.wait_free().await;
324        assert_eq!(Instant::now() - start, Duration::from_millis(250));
325        cap.wait_free().await;
326        cap.acquire().await;
327        assert_eq!(Instant::now() - start, Duration::from_millis(250));
328        assert_eq!(cap.recent(), 2);
329    }
330
331    /// A network with its own rate is held to it, whatever the global cap,
332    /// while another network goes at the default; a request whose network
333    /// is unknown takes only the global slot.
334    #[tokio::test(start_paused = true)]
335    async fn a_slow_network_is_held_to_its_rate() {
336        let limits = RequestLimits::new(0).with_network_rates(20, [(2001, 2)]);
337        assert!(limits.paces_networks());
338        let start = Instant::now();
339        let mut slow = Vec::new();
340        for device in 0..5 {
341            drop(limits.turn_on(device, Some(2001)).await);
342            slow.push((Instant::now() - start).as_millis());
343        }
344        assert_eq!(slow, [0, 500, 1000, 1500, 2000]);
345        let before = Instant::now();
346        for device in 10..14 {
347            drop(limits.turn_on(device, Some(0)).await);
348        }
349        assert_eq!(Instant::now() - before, Duration::from_millis(150));
350        let before = Instant::now();
351        drop(limits.turn_on(20, None).await);
352        assert_eq!(Instant::now(), before);
353    }
354
355    /// A charged call counts each request and the time it waited, and
356    /// takes its tokens from the account's bucket before the gate.
357    #[tokio::test(start_paused = true)]
358    async fn a_charged_call_counts_requests_and_waits() {
359        let limits = RequestLimits::new(10);
360        let bucket = Arc::new(TokenBucket::new(2, 1));
361        let account = Arc::new(Account::new(Some(bucket)));
362        charged(Arc::clone(&account), async {
363            for _ in 0..3 {
364                drop(limits.turn(1).await);
365            }
366            limits.slot(None).await;
367        })
368        .await;
369        assert_eq!(account.requests(), 4);
370        // The bucket's rate, 2 a second, outweighs the global 10.
371        assert_eq!(account.throttled(), Duration::from_millis(1500));
372        // Uncharged requests count nowhere.
373        drop(limits.turn(1).await);
374        assert_eq!(account.requests(), 4);
375    }
376}