kunobi_daemon/
admission.rs1use std::sync::{
3 Arc,
4 atomic::{AtomicU64, AtomicUsize, Ordering},
5};
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[cfg_attr(kani, derive(kani::Arbitrary))]
10pub enum Pool {
11 Handshake,
13 Application,
15 Control,
17}
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct Limits {
22 pub handshakes: usize,
24 pub application: usize,
26 pub control: usize,
28}
29impl Default for Limits {
30 fn default() -> Self {
31 Self {
32 handshakes: 32,
33 application: 256,
34 control: 32,
35 }
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[non_exhaustive]
42pub struct PoolSnapshot {
43 pub limit: usize,
45 pub active: usize,
47 pub rejected: u64,
49}
50struct Capacity {
51 limit: usize,
52 active: AtomicUsize,
53 rejected: AtomicU64,
54}
55
56pub struct Admission {
58 pools: [Capacity; 3],
59}
60impl Admission {
61 pub fn new(limits: Limits) -> Self {
63 Self {
64 pools: [limits.handshakes, limits.application, limits.control].map(|limit| Capacity {
65 limit,
66 active: AtomicUsize::new(0),
67 rejected: AtomicU64::new(0),
68 }),
69 }
70 }
71 pub fn try_acquire(self: &Arc<Self>, pool: Pool) -> Option<Permit> {
73 let capacity = &self.pools[pool as usize];
74 if capacity
75 .active
76 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |active| {
77 (active < capacity.limit).then(|| active + 1)
78 })
79 .is_err()
80 {
81 capacity.rejected.fetch_add(1, Ordering::Relaxed);
82 return None;
83 }
84 Some(Permit {
85 admission: Arc::clone(self),
86 pool,
87 })
88 }
89 pub fn snapshot(&self, pool: Pool) -> PoolSnapshot {
91 let capacity = &self.pools[pool as usize];
92 PoolSnapshot {
93 limit: capacity.limit,
94 active: capacity.active.load(Ordering::Relaxed),
95 rejected: capacity.rejected.load(Ordering::Relaxed),
96 }
97 }
98}
99#[must_use = "hold the permit until the session ends"]
101pub struct Permit {
102 admission: Arc<Admission>,
103 pool: Pool,
104}
105impl Drop for Permit {
106 fn drop(&mut self) {
107 self.admission.pools[self.pool as usize]
108 .active
109 .fetch_sub(1, Ordering::Relaxed);
110 }
111}
112
113#[cfg(kani)]
117mod proofs {
118 use super::*;
119
120 const STEPS: usize = 4;
123 const POOLS: [Pool; 3] = [Pool::Handshake, Pool::Application, Pool::Control];
124
125 fn small() -> usize {
126 usize::from(kani::any::<u8>() % 3)
127 }
128
129 #[kani::proof]
133 #[kani::unwind(5)]
134 fn each_pool_admits_up_to_its_own_limit_and_no_further() {
135 let limits = Limits {
136 handshakes: small(),
137 application: small(),
138 control: small(),
139 };
140 let limit = [limits.handshakes, limits.application, limits.control];
141 let admission = Arc::new(Admission::new(limits));
142 let mut held: [Option<Permit>; STEPS] = [const { None }; STEPS];
145 let mut active = [0usize; 3];
146 let mut rejected = [0u64; 3];
147
148 for step in 0..STEPS {
149 let release = kani::any_where(|at: &usize| *at < STEPS);
150 if kani::any() && held[release].is_some() {
151 let permit = held[release].take().unwrap();
152 active[permit.pool as usize] -= 1;
153 drop(permit);
154 } else {
155 let pool: Pool = kani::any();
156 let index = pool as usize;
157 match admission.try_acquire(pool) {
158 Some(permit) => {
159 assert!(active[index] < limit[index]);
160 active[index] += 1;
161 held[step] = Some(permit);
162 }
163 None => {
164 assert!(active[index] == limit[index]);
165 rejected[index] += 1;
166 }
167 }
168 }
169
170 for pool in POOLS {
171 let index = pool as usize;
172 let snapshot = admission.snapshot(pool);
173 assert!(snapshot.limit == limit[index]);
174 assert!(snapshot.active == active[index]);
175 assert!(snapshot.rejected == rejected[index]);
176 }
177 }
178 }
179}