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}