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 tokio::sync::{OwnedSemaphorePermit, Semaphore};
10
11#[path = "recovery_classifier.rs"]
12mod recovery_classifier;
13pub(super) use recovery_classifier::confirmed_class;
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct RequestAdmissionPolicy {
19 pub max_confirmed_in_flight: usize,
21 pub max_unconfirmed_in_flight: usize,
23 pub max_confirmed_in_flight_per_peer: usize,
26 pub max_unconfirmed_in_flight_per_peer: usize,
29 pub confirmed_recovery_reserve: usize,
32 pub max_recovery_in_flight_per_peer: usize,
35}
36
37impl Default for RequestAdmissionPolicy {
38 fn default() -> Self {
39 Self {
40 max_confirmed_in_flight: 64,
41 max_unconfirmed_in_flight: 32,
42 max_confirmed_in_flight_per_peer: 16,
43 max_unconfirmed_in_flight_per_peer: 8,
44 confirmed_recovery_reserve: 4,
45 max_recovery_in_flight_per_peer: 1,
46 }
47 }
48}
49
50impl RequestAdmissionPolicy {
51 pub fn validate(&self) -> Result<(), Error> {
53 for (name, value) in [
54 ("max_confirmed_in_flight", self.max_confirmed_in_flight),
55 ("max_unconfirmed_in_flight", self.max_unconfirmed_in_flight),
56 (
57 "max_recovery_in_flight_per_peer",
58 self.max_recovery_in_flight_per_peer,
59 ),
60 (
61 "max_confirmed_in_flight_per_peer",
62 self.max_confirmed_in_flight_per_peer,
63 ),
64 (
65 "max_unconfirmed_in_flight_per_peer",
66 self.max_unconfirmed_in_flight_per_peer,
67 ),
68 ] {
69 if value == 0 || value > Semaphore::MAX_PERMITS {
70 return Err(Error::Encoding(format!(
71 "{name} must be in 1..={}",
72 Semaphore::MAX_PERMITS
73 )));
74 }
75 }
76 if self.confirmed_recovery_reserve >= self.max_confirmed_in_flight {
77 return Err(Error::Encoding(
78 "confirmed_recovery_reserve must be smaller than max_confirmed_in_flight".into(),
79 ));
80 }
81 Ok(())
82 }
83}
84
85#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
89pub struct RequestAdmissionCounters {
90 pub recovery_active: usize,
92 pub recovery_admitted_total: u64,
94 pub recovery_overloaded_total: u64,
96 pub confirmed_active: usize,
98 pub confirmed_admitted_total: u64,
100 pub confirmed_overloaded_total: u64,
102 pub confirmed_global_overloaded_total: u64,
104 pub confirmed_peer_overloaded_total: u64,
106 pub confirmed_shutdown_rejected_total: u64,
108 pub unconfirmed_active: usize,
110 pub unconfirmed_admitted_total: u64,
112 pub unconfirmed_overloaded_total: u64,
114 pub unconfirmed_global_overloaded_total: u64,
116 pub unconfirmed_peer_overloaded_total: u64,
118 pub unconfirmed_shutdown_rejected_total: u64,
120 pub abort_active: usize,
122 pub abort_admitted_total: u64,
124 pub confirmed_fallback_dropped_total: u64,
126 pub abort_shutdown_rejected_total: u64,
128}
129
130#[derive(Clone, Copy)]
131pub(super) enum Class {
132 Confirmed = 0,
133 Unconfirmed = 1,
134 Abort = 2,
135 Recovery = 3,
136}
137
138#[derive(Debug, PartialEq, Eq)]
139pub(super) enum Rejection {
140 Closed,
141 Overloaded,
142}
143
144pub(super) struct Admission {
145 pools: [Arc<Pool>; 3],
146 recovery: Arc<Pool>,
147 recovery_enabled: bool,
148}
149
150struct Pool {
151 permits: Arc<Semaphore>,
152 peer_limit: usize,
153 peers: Mutex<HashMap<CanonicalRequester, usize>>,
154 active: AtomicUsize,
155 admitted: AtomicU64,
156 overloaded: AtomicU64,
157 global_overloaded: AtomicU64,
158 peer_overloaded: AtomicU64,
159 closed: AtomicU64,
160}
161
162impl Pool {
163 fn new(limit: usize, peer_limit: usize) -> Arc<Self> {
164 Arc::new(Self {
165 permits: Arc::new(Semaphore::new(limit)),
166 peer_limit,
167 peers: Mutex::new(HashMap::new()),
168 active: AtomicUsize::new(0),
169 admitted: AtomicU64::new(0),
170 overloaded: AtomicU64::new(0),
171 global_overloaded: AtomicU64::new(0),
172 peer_overloaded: AtomicU64::new(0),
173 closed: AtomicU64::new(0),
174 })
175 }
176}
177
178pub(super) struct Guard {
179 pool: Arc<Pool>,
180 aggregate: Option<Arc<Pool>>,
182 peer: Option<CanonicalRequester>,
183 _permit: OwnedSemaphorePermit,
184}
185
186impl Drop for Guard {
187 fn drop(&mut self) {
188 if let Some(peer) = &self.peer {
191 let mut peers = self.pool.peers.lock().unwrap_or_else(|e| e.into_inner());
192 let count = peers.get_mut(peer).expect("live peer registration");
193 *count -= 1;
194 if *count == 0 {
195 peers.remove(peer);
196 }
197 }
198 for pool in std::iter::once(&self.pool).chain(self.aggregate.iter()) {
199 pool.active.fetch_sub(1, Ordering::Relaxed);
200 }
201 }
202}
203
204impl Admission {
205 #[cfg(test)]
206 pub(super) fn peer_entries(&self) -> [usize; 3] {
207 let mut entries = std::array::from_fn(|i| self.pools[i].peers.lock().unwrap().len());
208 entries[0] += self.recovery.peers.lock().unwrap().len();
210 entries
211 }
212
213 pub(super) fn new(policy: RequestAdmissionPolicy) -> Result<Self, Error> {
214 policy.validate()?;
215 Ok(Self {
216 pools: [
217 Pool::new(
218 policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve,
219 policy
220 .max_confirmed_in_flight_per_peer
221 .min(policy.max_confirmed_in_flight - policy.confirmed_recovery_reserve),
222 ),
223 Pool::new(
224 policy.max_unconfirmed_in_flight,
225 policy
226 .max_unconfirmed_in_flight_per_peer
227 .min(policy.max_unconfirmed_in_flight),
228 ),
229 Pool::new(8, 8),
230 ],
231 recovery: Pool::new(
232 policy.confirmed_recovery_reserve,
233 policy
234 .max_recovery_in_flight_per_peer
235 .min(policy.confirmed_recovery_reserve),
236 ),
237 recovery_enabled: policy.confirmed_recovery_reserve != 0,
238 })
239 }
240
241 pub(super) fn try_enter(
243 &self,
244 class: Class,
245 peer: CanonicalRequester,
246 closed: bool,
247 ) -> Result<Guard, Rejection> {
248 let (pool, aggregate) = if matches!(class, Class::Recovery) {
249 if self.recovery_enabled {
250 (&self.recovery, Some(&self.pools[0]))
251 } else {
252 (&self.pools[0], None)
253 }
254 } else {
255 (&self.pools[class as usize], None)
256 };
257 if closed {
258 pool.closed.fetch_add(1, Ordering::Relaxed);
259 if let Some(total) = aggregate {
260 total.closed.fetch_add(1, Ordering::Relaxed);
261 }
262 return Err(Rejection::Closed);
263 }
264 let permit = Arc::clone(&pool.permits).try_acquire_owned().map_err(|_| {
265 pool.overloaded.fetch_add(1, Ordering::Relaxed);
266 pool.global_overloaded.fetch_add(1, Ordering::Relaxed);
267 if let Some(total) = aggregate {
268 total.overloaded.fetch_add(1, Ordering::Relaxed);
269 total.global_overloaded.fetch_add(1, Ordering::Relaxed);
270 }
271 Rejection::Overloaded
272 })?;
273 let peer = if matches!(class, Class::Abort) {
278 None
279 } else {
280 let mut peers = pool.peers.lock().unwrap_or_else(|e| e.into_inner());
281 if peers.get(&peer).copied().unwrap_or(0) >= pool.peer_limit {
282 pool.overloaded.fetch_add(1, Ordering::Relaxed);
283 pool.peer_overloaded.fetch_add(1, Ordering::Relaxed);
284 if let Some(total) = aggregate {
285 total.overloaded.fetch_add(1, Ordering::Relaxed);
286 total.peer_overloaded.fetch_add(1, Ordering::Relaxed);
287 }
288 return Err(Rejection::Overloaded);
289 }
290 *peers.entry(peer.clone()).or_default() += 1;
291 Some(peer)
292 };
293 pool.active.fetch_add(1, Ordering::Relaxed);
294 pool.admitted.fetch_add(1, Ordering::Relaxed);
295 if let Some(total) = aggregate {
296 total.active.fetch_add(1, Ordering::Relaxed);
297 total.admitted.fetch_add(1, Ordering::Relaxed);
298 }
299 Ok(Guard {
300 pool: Arc::clone(pool),
301 aggregate: aggregate.map(Arc::clone),
302 peer,
303 _permit: permit,
304 })
305 }
306
307 pub(super) fn snapshot(&self) -> RequestAdmissionCounters {
308 let [c, u, a] = &self.pools;
309 RequestAdmissionCounters {
310 recovery_active: self.recovery.active.load(Ordering::Relaxed),
311 recovery_admitted_total: self.recovery.admitted.load(Ordering::Relaxed),
312 recovery_overloaded_total: self.recovery.overloaded.load(Ordering::Relaxed),
313 confirmed_active: c.active.load(Ordering::Relaxed),
314 confirmed_admitted_total: c.admitted.load(Ordering::Relaxed),
315 confirmed_overloaded_total: c.overloaded.load(Ordering::Relaxed),
316 confirmed_global_overloaded_total: c.global_overloaded.load(Ordering::Relaxed),
317 confirmed_peer_overloaded_total: c.peer_overloaded.load(Ordering::Relaxed),
318 confirmed_shutdown_rejected_total: c.closed.load(Ordering::Relaxed),
319 unconfirmed_active: u.active.load(Ordering::Relaxed),
320 unconfirmed_admitted_total: u.admitted.load(Ordering::Relaxed),
321 unconfirmed_overloaded_total: u.overloaded.load(Ordering::Relaxed),
322 unconfirmed_global_overloaded_total: u.global_overloaded.load(Ordering::Relaxed),
323 unconfirmed_peer_overloaded_total: u.peer_overloaded.load(Ordering::Relaxed),
324 unconfirmed_shutdown_rejected_total: u.closed.load(Ordering::Relaxed),
325 abort_active: a.active.load(Ordering::Relaxed),
326 abort_admitted_total: a.admitted.load(Ordering::Relaxed),
327 confirmed_fallback_dropped_total: a.overloaded.load(Ordering::Relaxed),
328 abort_shutdown_rejected_total: a.closed.load(Ordering::Relaxed),
329 }
330 }
331}
332
333impl<T: super::TransportPort + 'static> super::ServerBuilder<T> {
334 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
336 self.config.request_admission_policy = policy;
337 self
338 }
339}
340
341impl super::BipServerBuilder {
342 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
344 self.config.request_admission_policy = policy;
345 self
346 }
347}
348
349#[cfg(feature = "sc-tls")]
350impl super::ScServerBuilder {
351 pub fn request_admission_policy(mut self, policy: RequestAdmissionPolicy) -> Self {
353 self.config.request_admission_policy = policy;
354 self
355 }
356}
357
358impl<T: super::TransportPort + 'static> super::BACnetServer<T> {
359 pub fn request_admission_counters(&self) -> RequestAdmissionCounters {
361 self.request_tasks.counters()
362 }
363}