1use std::collections::HashMap;
3use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
4use std::sync::{Arc, Mutex};
5
6use super::request_peer::CanonicalRequester;
7
8use bacnet_types::error::Error;
9use bacnet_types::map_hash::BuildMapHasher;
10use tokio::sync::Semaphore;
11
12#[path = "recovery_classifier.rs"]
13mod recovery_classifier;
14#[cfg(test)]
15#[path = "request_admission_slot_tests.rs"]
16mod slot_tests;
17pub(super) use recovery_classifier::confirmed_class;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct RequestAdmissionPolicy {
23 pub max_confirmed_in_flight: usize,
25 pub max_unconfirmed_in_flight: usize,
27 pub max_confirmed_in_flight_per_peer: usize,
30 pub max_unconfirmed_in_flight_per_peer: usize,
33 pub confirmed_recovery_reserve: usize,
36 pub max_recovery_in_flight_per_peer: usize,
39}
40
41impl Default for RequestAdmissionPolicy {
42 fn default() -> Self {
43 Self {
44 max_confirmed_in_flight: 64,
45 max_unconfirmed_in_flight: 32,
46 max_confirmed_in_flight_per_peer: 16,
47 max_unconfirmed_in_flight_per_peer: 8,
48 confirmed_recovery_reserve: 4,
49 max_recovery_in_flight_per_peer: 1,
50 }
51 }
52}
53
54impl RequestAdmissionPolicy {
55 pub fn validate(&self) -> Result<(), Error> {
58 for (name, value) in [
59 ("max_confirmed_in_flight", self.max_confirmed_in_flight),
60 ("max_unconfirmed_in_flight", self.max_unconfirmed_in_flight),
61 (
62 "max_recovery_in_flight_per_peer",
63 self.max_recovery_in_flight_per_peer,
64 ),
65 (
66 "max_confirmed_in_flight_per_peer",
67 self.max_confirmed_in_flight_per_peer,
68 ),
69 (
70 "max_unconfirmed_in_flight_per_peer",
71 self.max_unconfirmed_in_flight_per_peer,
72 ),
73 ] {
74 if value == 0 || value > Semaphore::MAX_PERMITS {
75 return Err(Error::Encoding(format!(
76 "{name} must be in 1..={}",
77 Semaphore::MAX_PERMITS
78 )));
79 }
80 }
81 if self.confirmed_recovery_reserve >= self.max_confirmed_in_flight {
82 return Err(Error::Encoding(
83 "confirmed_recovery_reserve must be smaller than max_confirmed_in_flight".into(),
84 ));
85 }
86 Ok(())
87 }
88}
89
90#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct RequestAdmissionCounters {
95 pub recovery_active: usize,
97 pub recovery_admitted_total: u64,
99 pub recovery_overloaded_total: u64,
101 pub confirmed_active: usize,
103 pub confirmed_admitted_total: u64,
105 pub confirmed_overloaded_total: u64,
107 pub confirmed_global_overloaded_total: u64,
109 pub confirmed_peer_overloaded_total: u64,
111 pub confirmed_shutdown_rejected_total: u64,
113 pub unconfirmed_active: usize,
115 pub unconfirmed_admitted_total: u64,
117 pub unconfirmed_overloaded_total: u64,
119 pub unconfirmed_global_overloaded_total: u64,
121 pub unconfirmed_peer_overloaded_total: u64,
123 pub unconfirmed_shutdown_rejected_total: u64,
125 pub abort_active: usize,
127 pub abort_admitted_total: u64,
129 pub confirmed_fallback_dropped_total: u64,
131 pub abort_shutdown_rejected_total: u64,
133}
134
135#[derive(Clone, Copy)]
136pub(super) enum Class {
137 Confirmed = 0,
138 Unconfirmed = 1,
139 Abort = 2,
140 Recovery = 3,
141}
142
143#[derive(Debug, PartialEq, Eq)]
144pub(super) enum Rejection {
145 Closed,
146 Overloaded,
147}
148
149pub(super) struct Admission {
150 pools: [Arc<Pool>; 3],
151 recovery: Arc<Pool>,
152 recovery_enabled: bool,
153}
154
155struct Pool {
160 limit: usize,
161 held: AtomicUsize,
162 peer_limit: usize,
163 peers: Mutex<HashMap<CanonicalRequester, usize, BuildMapHasher>>,
166 active: AtomicUsize,
167 admitted: AtomicU64,
168 overloaded: AtomicU64,
169 global_overloaded: AtomicU64,
170 peer_overloaded: AtomicU64,
171 closed: AtomicU64,
172}
173
174impl Pool {
175 fn new(limit: usize, peer_limit: usize) -> Arc<Self> {
176 Arc::new(Self {
177 limit,
178 held: AtomicUsize::new(0),
179 peer_limit,
180 peers: Mutex::new(HashMap::default()),
181 active: AtomicUsize::new(0),
182 admitted: AtomicU64::new(0),
183 overloaded: AtomicU64::new(0),
184 global_overloaded: AtomicU64::new(0),
185 peer_overloaded: AtomicU64::new(0),
186 closed: AtomicU64::new(0),
187 })
188 }
189
190 fn take_slot(&self) -> bool {
192 let mut held = self.held.load(Ordering::Relaxed);
193 while held < self.limit {
194 match self.held.compare_exchange_weak(
195 held,
196 held + 1,
197 Ordering::Acquire,
198 Ordering::Relaxed,
199 ) {
200 Ok(_) => return true,
201 Err(now) => held = now,
202 }
203 }
204 false
205 }
206
207 #[cfg(test)]
208 fn free_slots(&self) -> usize {
209 self.limit - self.held.load(Ordering::Relaxed)
210 }
211}
212
213pub(super) struct Guard {
214 pool: Arc<Pool>,
215 aggregate: Option<Arc<Pool>>,
217 peer: Option<CanonicalRequester>,
218}
219
220impl Drop for Guard {
221 fn drop(&mut self) {
222 if let Some(peer) = &self.peer {
226 let mut peers = self.pool.peers.lock().unwrap_or_else(|e| e.into_inner());
227 let count = peers.get_mut(peer).expect("live peer registration");
228 *count -= 1;
229 if *count == 0 {
230 peers.remove(peer);
231 }
232 }
233 for pool in std::iter::once(&self.pool).chain(self.aggregate.iter()) {
234 pool.active.fetch_sub(1, Ordering::Relaxed);
235 }
236 self.pool.held.fetch_sub(1, Ordering::Release);
237 }
238}
239
240impl Admission {
241 #[cfg(test)]
242 pub(super) fn peer_entries(&self) -> [usize; 3] {
243 let mut entries = std::array::from_fn(|i| self.pools[i].peers.lock().unwrap().len());
244 entries[0] += self.recovery.peers.lock().unwrap().len();
246 entries
247 }
248
249 pub(super) fn new(policy: RequestAdmissionPolicy) -> Result<Self, Error> {
250 policy.validate()?;
251 Ok(Self {
252 pools: [
253 Pool::new(
254 policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve,
255 policy
256 .max_confirmed_in_flight_per_peer
257 .min(policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve),
258 ),
259 Pool::new(
260 policy.max_unconfirmed_in_flight,
261 policy
262 .max_unconfirmed_in_flight_per_peer
263 .min(policy.max_unconfirmed_in_flight),
264 ),
265 Pool::new(8, 8),
266 ],
267 recovery: Pool::new(
268 policy.confirmed_recovery_reserve,
269 policy
270 .max_recovery_in_flight_per_peer
271 .min(policy.confirmed_recovery_reserve),
272 ),
273 recovery_enabled: policy.confirmed_recovery_reserve != 0,
274 })
275 }
276
277 pub(super) fn try_enter(
279 &self,
280 class: Class,
281 peer: CanonicalRequester,
282 closed: bool,
283 ) -> Result<Guard, Rejection> {
284 let (pool, aggregate) = if matches!(class, Class::Recovery) {
285 if self.recovery_enabled {
286 (&self.recovery, Some(&self.pools[0]))
287 } else {
288 (&self.pools[0], None)
289 }
290 } else {
291 (&self.pools[class as usize], None)
292 };
293 if closed {
294 pool.closed.fetch_add(1, Ordering::Relaxed);
295 if let Some(total) = aggregate {
296 total.closed.fetch_add(1, Ordering::Relaxed);
297 }
298 return Err(Rejection::Closed);
299 }
300 if !pool.take_slot() {
301 pool.overloaded.fetch_add(1, Ordering::Relaxed);
302 pool.global_overloaded.fetch_add(1, Ordering::Relaxed);
303 if let Some(total) = aggregate {
304 total.overloaded.fetch_add(1, Ordering::Relaxed);
305 total.global_overloaded.fetch_add(1, Ordering::Relaxed);
306 }
307 return Err(Rejection::Overloaded);
308 }
309 let peer = if matches!(class, Class::Abort) {
314 None
315 } else {
316 let mut peers = pool.peers.lock().unwrap_or_else(|e| e.into_inner());
317 let count = peers.entry(peer.clone()).or_insert(0);
319 if *count >= pool.peer_limit {
320 if *count == 0 {
322 peers.remove(&peer);
323 }
324 pool.overloaded.fetch_add(1, Ordering::Relaxed);
325 pool.peer_overloaded.fetch_add(1, Ordering::Relaxed);
326 if let Some(total) = aggregate {
327 total.overloaded.fetch_add(1, Ordering::Relaxed);
328 total.peer_overloaded.fetch_add(1, Ordering::Relaxed);
329 }
330 pool.held.fetch_sub(1, Ordering::Release);
331 return Err(Rejection::Overloaded);
332 }
333 *count += 1;
334 Some(peer)
335 };
336 pool.active.fetch_add(1, Ordering::Relaxed);
337 pool.admitted.fetch_add(1, Ordering::Relaxed);
338 if let Some(total) = aggregate {
339 total.active.fetch_add(1, Ordering::Relaxed);
340 total.admitted.fetch_add(1, Ordering::Relaxed);
341 }
342 Ok(Guard {
343 pool: Arc::clone(pool),
344 aggregate: aggregate.map(Arc::clone),
345 peer,
346 })
347 }
348
349 pub(super) fn snapshot(&self) -> RequestAdmissionCounters {
350 let [c, u, a] = &self.pools;
351 RequestAdmissionCounters {
352 recovery_active: self.recovery.active.load(Ordering::Relaxed),
353 recovery_admitted_total: self.recovery.admitted.load(Ordering::Relaxed),
354 recovery_overloaded_total: self.recovery.overloaded.load(Ordering::Relaxed),
355 confirmed_active: c.active.load(Ordering::Relaxed),
356 confirmed_admitted_total: c.admitted.load(Ordering::Relaxed),
357 confirmed_overloaded_total: c.overloaded.load(Ordering::Relaxed),
358 confirmed_global_overloaded_total: c.global_overloaded.load(Ordering::Relaxed),
359 confirmed_peer_overloaded_total: c.peer_overloaded.load(Ordering::Relaxed),
360 confirmed_shutdown_rejected_total: c.closed.load(Ordering::Relaxed),
361 unconfirmed_active: u.active.load(Ordering::Relaxed),
362 unconfirmed_admitted_total: u.admitted.load(Ordering::Relaxed),
363 unconfirmed_overloaded_total: u.overloaded.load(Ordering::Relaxed),
364 unconfirmed_global_overloaded_total: u.global_overloaded.load(Ordering::Relaxed),
365 unconfirmed_peer_overloaded_total: u.peer_overloaded.load(Ordering::Relaxed),
366 unconfirmed_shutdown_rejected_total: u.closed.load(Ordering::Relaxed),
367 abort_active: a.active.load(Ordering::Relaxed),
368 abort_admitted_total: a.admitted.load(Ordering::Relaxed),
369 confirmed_fallback_dropped_total: a.overloaded.load(Ordering::Relaxed),
370 abort_shutdown_rejected_total: a.closed.load(Ordering::Relaxed),
371 }
372 }
373}
374
375impl<T: super::TransportPort + 'static> super::ServerBuilder<T> {
376 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
378 self.config.request_admission_policy = policy;
379 self
380 }
381}
382
383impl super::BipServerBuilder {
384 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
386 self.config.request_admission_policy = policy;
387 self
388 }
389}
390
391#[cfg(feature = "sc-tls")]
392impl super::ScServerBuilder {
393 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
395 self.config.request_admission_policy = policy;
396 self
397 }
398}
399
400impl<T: super::TransportPort + 'static> super::BACnetServer<T> {
401 pub fn request_admission_counters(&self) -> RequestAdmissionCounters {
403 self.request_tasks.counters()
404 }
405}