1use std::fmt;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::{Condvar, Mutex, MutexGuard, PoisonError};
4use std::time::{Duration, Instant};
5
6pub(crate) fn jitter() -> f64 {
8 static STATE: AtomicU64 = AtomicU64::new(0);
9 let mut x = STATE.load(Ordering::Relaxed);
10 if x == 0 {
11 let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(1, |d| d.as_nanos() as u64);
12 x = nanos ^ (u64::from(std::process::id()) << 32) | 1;
13 }
14 x ^= x << 13;
15 x ^= x >> 7;
16 x ^= x << 17;
17 STATE.store(x, Ordering::Relaxed);
18 (x >> 11) as f64 / (1u64 << 53) as f64
19}
20
21struct Gate {
22 limit: f64,
23 in_flight: usize,
24 paused_until: Option<Instant>,
25}
26
27pub struct AdaptiveLimiter {
37 max: f64,
38 pause: Duration,
39 gate: Mutex<Gate>,
40 cv: Condvar,
41}
42
43impl AdaptiveLimiter {
44 pub fn new(max_concurrency: usize, pause: Duration) -> Self {
47 let max = max_concurrency.max(1) as f64;
48 AdaptiveLimiter { max, pause, gate: Mutex::new(Gate { limit: max, in_flight: 0, paused_until: None }), cv: Condvar::new() }
49 }
50
51 fn gate(&self) -> MutexGuard<'_, Gate> {
54 self.gate.lock().unwrap_or_else(PoisonError::into_inner)
55 }
56
57 pub fn limit(&self) -> f64 {
61 self.gate().limit
62 }
63
64 pub fn in_flight(&self) -> usize {
66 self.gate().in_flight
67 }
68
69 pub fn acquire(&self) {
72 let mut gate = self.gate();
73 loop {
74 let wait = gate.paused_until.and_then(|t| t.checked_duration_since(Instant::now())).filter(|d| !d.is_zero());
75 match wait {
76 None if gate.in_flight < (gate.limit as usize).max(1) => {
77 gate.in_flight += 1;
78 return;
79 }
80 None => gate = self.cv.wait(gate).unwrap_or_else(PoisonError::into_inner),
81 Some(d) => {
82 gate = self.cv.wait_timeout(gate, d.max(Duration::from_millis(50))).unwrap_or_else(PoisonError::into_inner).0;
83 }
84 }
85 }
86 }
87
88 pub fn release(&self, throttled: bool) {
93 let mut gate = self.gate();
94 gate.in_flight = gate.in_flight.saturating_sub(1);
95 let now = Instant::now();
96 if throttled {
97 if gate.paused_until.is_none_or(|t| now >= t) {
98 gate.limit = (gate.limit / 2.0).max(1.0);
99 let pause = Duration::try_from_secs_f64(self.pause.as_secs_f64() * (1.0 + jitter())).ok();
101 gate.paused_until = pause.and_then(|pause| now.checked_add(pause));
102 }
103 } else {
104 gate.limit = self.max.min(gate.limit + 1.0 / gate.limit.max(1.0));
105 }
106 self.cv.notify_all();
107 }
108}
109
110impl fmt::Debug for AdaptiveLimiter {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 let gate = self.gate();
113 f.debug_struct("AdaptiveLimiter")
114 .field("max", &self.max)
115 .field("pause", &self.pause)
116 .field("limit", &gate.limit)
117 .field("in_flight", &gate.in_flight)
118 .finish_non_exhaustive()
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::sync::atomic::AtomicUsize;
126 use std::sync::Arc;
127
128 #[test]
129 fn jitter_is_in_the_unit_interval_and_varies() {
130 let draws: Vec<f64> = (0..64).map(|_| jitter()).collect();
131 assert!(draws.iter().all(|&x| (0.0..1.0).contains(&x)));
132 assert!(draws.windows(2).any(|w| w[0] != w[1]));
133 }
134
135 #[test]
136 fn throttling_halves_the_limit_once_per_episode_and_success_grows_it_back() {
137 let limiter = AdaptiveLimiter::new(8, Duration::ZERO);
138 assert_eq!(limiter.limit(), 8.0);
139 for _ in 0..3 {
140 limiter.acquire();
141 }
142 assert_eq!(limiter.in_flight(), 3);
143 limiter.release(true);
144 limiter.release(true); assert!(limiter.limit() <= 4.0 && limiter.limit() >= 2.0, "{}", limiter.limit());
146 limiter.release(false);
147 assert_eq!(limiter.in_flight(), 0);
148 for _ in 0..200 {
149 limiter.acquire();
150 limiter.release(false);
151 }
152 assert_eq!(limiter.limit(), 8.0);
153 }
154
155 #[test]
156 fn the_limit_never_drops_below_one() {
157 let limiter = AdaptiveLimiter::new(0, Duration::ZERO);
158 assert_eq!(limiter.limit(), 1.0);
159 for _ in 0..5 {
160 limiter.acquire();
161 std::thread::sleep(Duration::from_millis(1));
162 limiter.release(true);
163 }
164 assert_eq!(limiter.limit(), 1.0);
165 }
166
167 #[test]
168 fn an_unmatched_release_and_an_unrepresentable_pause_do_not_panic() {
169 let limiter = AdaptiveLimiter::new(2, Duration::MAX);
170 limiter.release(false);
171 assert_eq!(limiter.in_flight(), 0);
172 limiter.acquire();
173 limiter.release(true);
174 assert_eq!((limiter.limit(), limiter.in_flight()), (1.0, 0));
175 limiter.acquire(); limiter.release(false);
177 let shown = format!("{limiter:?}");
178 assert!(shown.starts_with("AdaptiveLimiter {") && shown.contains("in_flight: 0"), "{shown}");
179 }
180
181 #[test]
182 fn acquire_blocks_while_the_gate_is_full() {
183 let limiter = Arc::new(AdaptiveLimiter::new(2, Duration::ZERO));
184 let peak = Arc::new(AtomicUsize::new(0));
185 let running = Arc::new(AtomicUsize::new(0));
186 let workers: Vec<_> = (0..8)
187 .map(|_| {
188 let (limiter, peak, running) = (Arc::clone(&limiter), Arc::clone(&peak), Arc::clone(&running));
189 std::thread::spawn(move || {
190 limiter.acquire();
191 let now = running.fetch_add(1, Ordering::SeqCst) + 1;
192 peak.fetch_max(now, Ordering::SeqCst);
193 std::thread::sleep(Duration::from_millis(5));
194 running.fetch_sub(1, Ordering::SeqCst);
195 limiter.release(false);
196 })
197 })
198 .collect();
199 for worker in workers {
200 worker.join().unwrap();
201 }
202 assert!(peak.load(Ordering::SeqCst) <= 2, "{}", peak.load(Ordering::SeqCst));
203 assert_eq!(limiter.in_flight(), 0);
204 }
205}