confium_coordinator/coordinator/
backpressure.rs1use std::sync::atomic::{AtomicUsize, Ordering};
11
12#[derive(Debug, Clone)]
14pub struct BackpressureConfig {
15 pub max_active_sessions: usize,
17}
18
19impl Default for BackpressureConfig {
20 fn default() -> Self {
21 Self {
22 max_active_sessions: 100,
23 }
24 }
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
29pub enum BackpressureError {
30 #[error("at capacity: {active}/{max}")]
32 AtCapacity {
33 active: usize,
35 max: usize,
37 },
38}
39
40pub struct BackpressureGate {
42 config: BackpressureConfig,
43 active: AtomicUsize,
44}
45
46impl BackpressureGate {
47 pub fn new(config: BackpressureConfig) -> Self {
49 Self {
50 config,
51 active: AtomicUsize::new(0),
52 }
53 }
54
55 pub fn active_count(&self) -> usize {
57 self.active.load(Ordering::SeqCst)
58 }
59
60 pub fn max_sessions(&self) -> usize {
62 self.config.max_active_sessions
63 }
64
65 pub fn try_acquire(&self) -> Result<ActiveSlot<'_>, BackpressureError> {
69 let max = self.config.max_active_sessions;
70 if max == 0 {
71 return Ok(ActiveSlot {
72 gate: self,
73 unlimited: true,
74 });
75 }
76 let current = self.active.fetch_add(1, Ordering::SeqCst);
77 if current >= max {
78 self.active.fetch_sub(1, Ordering::SeqCst);
79 return Err(BackpressureError::AtCapacity {
80 active: current,
81 max,
82 });
83 }
84 Ok(ActiveSlot {
85 gate: self,
86 unlimited: false,
87 })
88 }
89
90 fn release(&self) {
92 self.active.fetch_sub(1, Ordering::SeqCst);
93 }
94
95 pub fn is_at_capacity(&self) -> bool {
97 let max = self.config.max_active_sessions;
98 max > 0 && self.active_count() >= max
99 }
100}
101
102pub struct ActiveSlot<'a> {
105 gate: &'a BackpressureGate,
106 unlimited: bool,
107}
108
109impl<'a> std::fmt::Debug for ActiveSlot<'a> {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("ActiveSlot")
112 .field("unlimited", &self.unlimited)
113 .finish()
114 }
115}
116
117impl<'a> Drop for ActiveSlot<'a> {
118 fn drop(&mut self) {
119 if !self.unlimited {
120 self.gate.release();
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn default_max_is_100() {
131 let gate = BackpressureGate::new(BackpressureConfig::default());
132 assert_eq!(gate.max_sessions(), 100);
133 }
134
135 #[test]
136 fn acquire_increments_count() {
137 let gate = BackpressureGate::new(BackpressureConfig {
138 max_active_sessions: 5,
139 });
140 assert_eq!(gate.active_count(), 0);
141 let slot = gate.try_acquire().unwrap();
142 assert_eq!(gate.active_count(), 1);
143 drop(slot);
144 assert_eq!(gate.active_count(), 0);
145 }
146
147 #[test]
148 fn at_capacity_rejects() {
149 let gate = BackpressureGate::new(BackpressureConfig {
150 max_active_sessions: 2,
151 });
152 let _s1 = gate.try_acquire().unwrap();
153 let _s2 = gate.try_acquire().unwrap();
154 let result = gate.try_acquire();
155 assert_eq!(
156 result.unwrap_err(),
157 BackpressureError::AtCapacity { active: 2, max: 2 }
158 );
159 }
160
161 #[test]
162 fn release_allows_new_acquire() {
163 let gate = BackpressureGate::new(BackpressureConfig {
164 max_active_sessions: 1,
165 });
166 {
167 let _slot = gate.try_acquire().unwrap();
168 assert!(gate.try_acquire().is_err());
169 }
170 assert!(gate.try_acquire().is_ok());
171 }
172
173 #[test]
174 fn unlimited_mode_never_rejects() {
175 let gate = BackpressureGate::new(BackpressureConfig {
176 max_active_sessions: 0,
177 });
178 for _ in 0..1000 {
179 assert!(gate.try_acquire().is_ok());
180 }
181 }
182
183 #[test]
184 fn is_at_capacity_reflects_state() {
185 let gate = BackpressureGate::new(BackpressureConfig {
186 max_active_sessions: 2,
187 });
188 assert!(!gate.is_at_capacity());
189 let _s1 = gate.try_acquire().unwrap();
190 assert!(!gate.is_at_capacity());
191 let _s2 = gate.try_acquire().unwrap();
192 assert!(gate.is_at_capacity());
193 }
194
195 #[test]
196 fn unlimited_is_never_at_capacity() {
197 let gate = BackpressureGate::new(BackpressureConfig {
198 max_active_sessions: 0,
199 });
200 assert!(!gate.is_at_capacity());
201 }
202
203 #[test]
204 fn slot_release_on_drop() {
205 let gate = BackpressureGate::new(BackpressureConfig {
206 max_active_sessions: 1,
207 });
208 {
209 let _slot = gate.try_acquire().unwrap();
210 assert_eq!(gate.active_count(), 1);
211 }
212 assert_eq!(gate.active_count(), 0);
213 }
214
215 #[test]
216 fn concurrent_acquires_respect_limit() {
217 use std::sync::Arc;
218 use std::thread;
219
220 let gate = Arc::new(BackpressureGate::new(BackpressureConfig {
221 max_active_sessions: 3,
222 }));
223 let mut handles = Vec::new();
224 let success_count = Arc::new(AtomicUsize::new(0));
225
226 for _ in 0..10 {
227 let gate = Arc::clone(&gate);
228 let counter = Arc::clone(&success_count);
229 handles.push(thread::spawn(move || {
230 if let Ok(_slot) = gate.try_acquire() {
231 counter.fetch_add(1, Ordering::SeqCst);
232 thread::sleep(std::time::Duration::from_millis(10));
233 }
234 }));
235 }
236
237 for h in handles {
238 h.join().unwrap();
239 }
240
241 assert_eq!(success_count.load(Ordering::SeqCst), 3);
242 }
243}