1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant};
6
7use tokio::sync::{OwnedSemaphorePermit, Semaphore};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
11pub enum WorkPriority {
12 Notification,
14 Normal,
16 Control,
18 Emergency,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ProviderLimits {
25 pub concurrency: usize,
27 pub reserved_control_concurrency: usize,
29 pub tenant_concurrency: usize,
31 pub failure_threshold: u32,
33 pub recovery_after: Duration,
35 pub max_elapsed: Duration,
37 pub initial_backoff: Duration,
39 pub max_backoff: Duration,
41}
42
43impl ProviderLimits {
44 pub fn validate(&self) -> Result<(), String> {
46 if self.concurrency == 0
47 || self.tenant_concurrency == 0
48 || self.failure_threshold == 0
49 || self.recovery_after.is_zero()
50 || self.max_elapsed.is_zero()
51 {
52 return Err("provider limits must be positive".into());
53 }
54 if self.tenant_concurrency > self.concurrency {
55 return Err("tenant concurrency cannot exceed provider concurrency".into());
56 }
57 if self.reserved_control_concurrency >= self.concurrency {
58 return Err("reserved control concurrency must be below provider concurrency".into());
59 }
60 if self.initial_backoff.is_zero() || self.initial_backoff > self.max_backoff {
61 return Err("provider backoff range is invalid".into());
62 }
63 Ok(())
64 }
65
66 pub fn backoff(&self, attempt: u32, jitter_seed: u64) -> Duration {
68 let factor = 1_u32.checked_shl(attempt.min(20)).unwrap_or(u32::MAX);
69 let base = self
70 .initial_backoff
71 .saturating_mul(factor)
72 .min(self.max_backoff);
73 let jitter_ceiling = (base.as_millis() / 4).max(1) as u64;
74 base.saturating_add(Duration::from_millis(jitter_seed % jitter_ceiling))
75 .min(self.max_backoff)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub enum ProviderBlock {
82 CircuitOpen {
84 retry_after: Duration,
86 },
87 Saturated,
89}
90
91struct Circuit {
92 consecutive_failures: u32,
93 opened_at: Option<Instant>,
94 probe_in_flight: bool,
95}
96
97const TENANT_SWEEP_THRESHOLD: usize = 1_024;
101
102pub struct ProviderGate {
104 limits: ProviderLimits,
105 global: Arc<Semaphore>,
106 normal: Arc<Semaphore>,
107 tenants: Mutex<HashMap<String, Arc<Semaphore>>>,
108 circuit: Arc<Mutex<Circuit>>,
109}
110
111pub struct ProviderPermit {
116 _global: OwnedSemaphorePermit,
117 _normal: Option<OwnedSemaphorePermit>,
118 _tenant: Option<OwnedSemaphorePermit>,
119 is_probe: bool,
120 circuit: Arc<Mutex<Circuit>>,
121}
122
123impl Drop for ProviderPermit {
124 fn drop(&mut self) {
125 if self.is_probe {
126 self.circuit
127 .lock()
128 .unwrap_or_else(std::sync::PoisonError::into_inner)
129 .probe_in_flight = false;
130 }
131 }
132}
133
134impl ProviderGate {
135 pub fn new(limits: ProviderLimits) -> Result<Self, String> {
137 limits.validate()?;
138 Ok(Self {
139 global: Arc::new(Semaphore::new(limits.concurrency)),
140 normal: Arc::new(Semaphore::new(
141 limits.concurrency - limits.reserved_control_concurrency,
142 )),
143 limits,
144 tenants: Mutex::new(HashMap::new()),
145 circuit: Arc::new(Mutex::new(Circuit {
146 consecutive_failures: 0,
147 opened_at: None,
148 probe_in_flight: false,
149 })),
150 })
151 }
152
153 pub fn try_acquire(
155 &self,
156 tenant_id: &str,
157 priority: WorkPriority,
158 now: Instant,
159 ) -> Result<ProviderPermit, ProviderBlock> {
160 let is_probe = {
161 let mut circuit = self
162 .circuit
163 .lock()
164 .unwrap_or_else(std::sync::PoisonError::into_inner);
165 match circuit.opened_at {
166 None => false,
167 Some(opened_at) => {
168 let elapsed = now.saturating_duration_since(opened_at);
169 if elapsed < self.limits.recovery_after || circuit.probe_in_flight {
170 return Err(ProviderBlock::CircuitOpen {
171 retry_after: self.limits.recovery_after.saturating_sub(elapsed),
172 });
173 }
174 circuit.probe_in_flight = true;
175 true
176 }
177 }
178 };
179 let normal = if matches!(priority, WorkPriority::Notification | WorkPriority::Normal) {
180 match self.normal.clone().try_acquire_owned() {
181 Ok(permit) => Some(permit),
182 Err(_) => {
183 self.release_probe(is_probe);
184 return Err(ProviderBlock::Saturated);
185 }
186 }
187 } else {
188 None
189 };
190 let global = match self.global.clone().try_acquire_owned() {
191 Ok(permit) => permit,
192 Err(_) => {
193 self.release_probe(is_probe);
194 return Err(ProviderBlock::Saturated);
195 }
196 };
197 let tenant = if priority == WorkPriority::Emergency {
198 None
199 } else {
200 let tenant = {
201 let mut tenants = self
202 .tenants
203 .lock()
204 .unwrap_or_else(std::sync::PoisonError::into_inner);
205 if tenants.len() >= TENANT_SWEEP_THRESHOLD {
206 let full = self.limits.tenant_concurrency;
207 tenants.retain(|_, semaphore| semaphore.available_permits() < full);
208 }
209 tenants
210 .entry(tenant_id.to_owned())
211 .or_insert_with(|| Arc::new(Semaphore::new(self.limits.tenant_concurrency)))
212 .clone()
213 };
214 match tenant.try_acquire_owned() {
215 Ok(permit) => Some(permit),
216 Err(_) => {
217 self.release_probe(is_probe);
218 return Err(ProviderBlock::Saturated);
219 }
220 }
221 };
222 Ok(ProviderPermit {
223 _global: global,
224 _normal: normal,
225 _tenant: tenant,
226 is_probe,
227 circuit: self.circuit.clone(),
228 })
229 }
230
231 pub fn record_success(&self, permit: ProviderPermit) {
233 let mut circuit = self
234 .circuit
235 .lock()
236 .unwrap_or_else(std::sync::PoisonError::into_inner);
237 circuit.consecutive_failures = 0;
238 circuit.opened_at = None;
239 drop(circuit);
240 drop(permit);
241 }
242
243 pub fn record_failure(&self, permit: ProviderPermit, now: Instant) {
245 let mut circuit = self
246 .circuit
247 .lock()
248 .unwrap_or_else(std::sync::PoisonError::into_inner);
249 circuit.consecutive_failures = circuit.consecutive_failures.saturating_add(1);
250 if permit.is_probe || circuit.consecutive_failures >= self.limits.failure_threshold {
251 circuit.opened_at = Some(now);
252 }
253 drop(circuit);
254 drop(permit);
255 }
256
257 pub fn release(&self, permit: ProviderPermit) {
261 drop(permit);
262 }
263
264 fn release_probe(&self, is_probe: bool) {
265 if is_probe {
266 self.circuit
267 .lock()
268 .unwrap_or_else(std::sync::PoisonError::into_inner)
269 .probe_in_flight = false;
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277
278 fn limits() -> ProviderLimits {
279 ProviderLimits {
280 concurrency: 2,
281 reserved_control_concurrency: 1,
282 tenant_concurrency: 1,
283 failure_threshold: 2,
284 recovery_after: Duration::from_secs(10),
285 max_elapsed: Duration::from_secs(30),
286 initial_backoff: Duration::from_millis(100),
287 max_backoff: Duration::from_secs(1),
288 }
289 }
290
291 #[test]
292 fn tenant_bulkhead_and_circuit_recovery_are_bounded() {
293 let gate = ProviderGate::new(limits()).unwrap();
294 let now = Instant::now();
295 let first = gate.try_acquire("one", WorkPriority::Normal, now).unwrap();
296 let emergency = gate
297 .try_acquire("one", WorkPriority::Emergency, now)
298 .expect("reserved control capacity must remain available");
299 gate.record_success(emergency);
300 assert_eq!(
301 gate.try_acquire("two", WorkPriority::Normal, now).err(),
302 Some(ProviderBlock::Saturated)
303 );
304 gate.record_failure(first, now);
305 let second = gate.try_acquire("two", WorkPriority::Normal, now).unwrap();
306 gate.record_failure(second, now);
307 assert!(matches!(
308 gate.try_acquire("one", WorkPriority::Normal, now),
309 Err(ProviderBlock::CircuitOpen { .. })
310 ));
311 let probe = gate
312 .try_acquire("one", WorkPriority::Normal, now + Duration::from_secs(10))
313 .unwrap();
314 gate.record_success(probe);
315 assert!(gate
316 .try_acquire("one", WorkPriority::Normal, now + Duration::from_secs(10))
317 .is_ok());
318 }
319
320 #[test]
321 fn dropped_probe_permit_does_not_wedge_the_circuit() {
322 let gate = ProviderGate::new(limits()).unwrap();
323 let now = Instant::now();
324 for tenant in ["one", "two"] {
325 let permit = gate.try_acquire(tenant, WorkPriority::Normal, now).unwrap();
326 gate.record_failure(permit, now);
327 }
328 let later = now + Duration::from_secs(10);
329 let probe = gate
330 .try_acquire("one", WorkPriority::Normal, later)
331 .unwrap();
332 assert!(matches!(
333 gate.try_acquire("two", WorkPriority::Normal, later),
334 Err(ProviderBlock::CircuitOpen { .. })
335 ));
336 gate.release(probe);
337 let probe = gate
338 .try_acquire("two", WorkPriority::Normal, later)
339 .expect("released probe must allow the next probe");
340 gate.record_success(probe);
341 assert!(gate.try_acquire("one", WorkPriority::Normal, later).is_ok());
342 }
343
344 #[test]
345 fn tenant_map_is_swept_once_it_grows() {
346 let gate = ProviderGate::new(ProviderLimits {
347 concurrency: 4,
348 tenant_concurrency: 1,
349 ..limits()
350 })
351 .unwrap();
352 let now = Instant::now();
353 for index in 0..TENANT_SWEEP_THRESHOLD + 5 {
354 let permit = gate
355 .try_acquire(&format!("tenant-{index}"), WorkPriority::Normal, now)
356 .unwrap();
357 gate.record_success(permit);
358 }
359 assert!(gate.tenants.lock().unwrap().len() <= TENANT_SWEEP_THRESHOLD + 1);
360 }
361
362 #[test]
363 fn backoff_is_capped() {
364 let limits = limits();
365 assert_eq!(limits.backoff(30, 99), Duration::from_secs(1));
366 }
367}