1use std::{
2 collections::VecDeque,
3 sync::{
4 atomic::{AtomicUsize, Ordering},
5 Arc, Mutex,
6 },
7 time::{Duration, Instant},
8};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LoadShedderMode {
13 Latency,
15 CpuThroughput,
17}
18
19#[derive(Debug, Clone, Copy)]
21pub struct LoadShedderConfig {
22 pub max_concurrency: usize,
23 pub target_latency: Duration,
24 pub sample_window: usize,
25 pub mode: LoadShedderMode,
26 pub cpu_threshold: f64,
27 pub bucket_duration: Duration,
28 pub bucket_count: usize,
29 pub cooldown: Duration,
30 pub in_flight_smoothing: f64,
31}
32
33impl LoadShedderConfig {
34 pub fn new(max_concurrency: usize, target_latency: Duration) -> Self {
36 assert!(
37 max_concurrency > 0,
38 "maximum concurrency must be greater than zero"
39 );
40 assert!(
41 !target_latency.is_zero(),
42 "target latency must be greater than zero"
43 );
44
45 Self {
46 max_concurrency,
47 target_latency,
48 sample_window: 32,
49 mode: LoadShedderMode::Latency,
50 cpu_threshold: 0.9,
51 bucket_duration: Duration::from_secs(1),
52 bucket_count: 10,
53 cooldown: Duration::from_secs(1),
54 in_flight_smoothing: 0.5,
55 }
56 }
57
58 pub fn production(max_concurrency: usize) -> Self {
60 Self::new(max_concurrency, Duration::from_millis(1))
61 .with_mode(LoadShedderMode::CpuThroughput)
62 }
63
64 pub fn with_mode(mut self, mode: LoadShedderMode) -> Self {
65 self.mode = mode;
66 self
67 }
68
69 pub fn with_sample_window(mut self, sample_window: usize) -> Self {
70 assert!(sample_window > 0, "sample window must be greater than zero");
71 self.sample_window = sample_window;
72 self
73 }
74
75 pub fn with_cpu_threshold(mut self, threshold: f64) -> Self {
76 assert!(
77 (0.0..=1.0).contains(&threshold) && threshold > 0.0,
78 "CPU threshold must be in (0, 1]"
79 );
80 self.cpu_threshold = threshold;
81 self
82 }
83
84 pub fn with_rolling_window(mut self, bucket_duration: Duration, bucket_count: usize) -> Self {
85 assert!(
86 !bucket_duration.is_zero(),
87 "bucket duration must be greater than zero"
88 );
89 assert!(bucket_count > 0, "bucket count must be greater than zero");
90 self.bucket_duration = bucket_duration;
91 self.bucket_count = bucket_count;
92 self
93 }
94
95 pub fn with_cooldown(mut self, cooldown: Duration) -> Self {
96 assert!(!cooldown.is_zero(), "cooldown must be greater than zero");
97 self.cooldown = cooldown;
98 self
99 }
100
101 pub fn with_in_flight_smoothing(mut self, smoothing: f64) -> Self {
102 assert!(
103 (0.0..=1.0).contains(&smoothing) && smoothing > 0.0,
104 "in-flight smoothing must be in (0, 1]"
105 );
106 self.in_flight_smoothing = smoothing;
107 self
108 }
109}
110
111#[derive(Clone, Copy)]
112struct Bucket {
113 started_at: Instant,
114 completed: usize,
115 minimum_latency: Option<Duration>,
116}
117
118struct ShedderState {
119 current_limit: usize,
120 sample_count: usize,
121 total_latency: Duration,
122 buckets: VecDeque<Bucket>,
123 smoothed_in_flight: f64,
124 cooldown_until: Option<Instant>,
125}
126
127trait CpuSource: Send + Sync {
128 fn usage(&self) -> f64;
129}
130
131struct Inner {
132 config: LoadShedderConfig,
133 in_flight: AtomicUsize,
134 state: Mutex<ShedderState>,
135 cpu: Arc<dyn CpuSource>,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq)]
140pub struct LoadShedderSnapshot {
141 pub in_flight: usize,
142 pub smoothed_in_flight: f64,
143 pub current_limit: usize,
144 pub maximum_throughput: f64,
145 pub minimum_latency: Option<Duration>,
146 pub cooling_down: bool,
147}
148
149#[derive(Clone)]
151pub struct AdaptiveShedder {
152 inner: Arc<Inner>,
153}
154
155impl AdaptiveShedder {
156 pub fn new(config: LoadShedderConfig) -> Self {
157 Self::with_cpu_source(config, Arc::new(ProcessCpuSource::new()))
158 }
159
160 fn with_cpu_source(config: LoadShedderConfig, cpu: Arc<dyn CpuSource>) -> Self {
161 let now = Instant::now();
162 Self {
163 inner: Arc::new(Inner {
164 config,
165 state: Mutex::new(ShedderState {
166 current_limit: config.max_concurrency,
167 sample_count: 0,
168 total_latency: Duration::ZERO,
169 buckets: VecDeque::from([Bucket {
170 started_at: now,
171 completed: 0,
172 minimum_latency: None,
173 }]),
174 smoothed_in_flight: 0.0,
175 cooldown_until: None,
176 }),
177 in_flight: AtomicUsize::new(0),
178 cpu,
179 }),
180 }
181 }
182
183 pub fn try_acquire(&self) -> Option<ShedPermit> {
185 loop {
186 let active = self.inner.in_flight.load(Ordering::Acquire);
187 let now = Instant::now();
188 let mut state = self
189 .inner
190 .state
191 .lock()
192 .expect("load shedder state lock poisoned");
193 rotate_buckets(&mut state, &self.inner.config, now);
194
195 let limit = match self.inner.config.mode {
196 LoadShedderMode::Latency => state.current_limit,
197 LoadShedderMode::CpuThroughput => production_limit(&state, &self.inner.config),
198 };
199 state.current_limit = limit;
200 let smoothed = state.smoothed_in_flight;
201 let cooling_down = state.cooldown_until.is_some_and(|until| now < until);
202 let cpu_overloaded = self.inner.cpu.usage() >= self.inner.config.cpu_threshold;
203 let fixed_capacity_exhausted = active >= self.inner.config.max_concurrency;
204 let dynamically_overloaded = self.inner.config.mode == LoadShedderMode::CpuThroughput
205 && (cpu_overloaded || cooling_down)
206 && active >= limit
207 && smoothed >= limit as f64;
208 if fixed_capacity_exhausted
209 || (self.inner.config.mode == LoadShedderMode::Latency && active >= limit)
210 || dynamically_overloaded
211 {
212 if cpu_overloaded {
213 state.cooldown_until = Some(now + self.inner.config.cooldown);
214 }
215 return None;
216 }
217
218 drop(state);
219 if self
220 .inner
221 .in_flight
222 .compare_exchange_weak(active, active + 1, Ordering::AcqRel, Ordering::Acquire)
223 .is_ok()
224 {
225 let mut state = self
226 .inner
227 .state
228 .lock()
229 .expect("load shedder state lock poisoned");
230 let alpha = self.inner.config.in_flight_smoothing;
231 state.smoothed_in_flight = if state.smoothed_in_flight == 0.0 {
232 (active + 1) as f64
233 } else {
234 alpha * (active + 1) as f64 + (1.0 - alpha) * state.smoothed_in_flight
235 };
236 return Some(ShedPermit {
237 inner: Arc::clone(&self.inner),
238 started_at: now,
239 });
240 }
241 }
242 }
243
244 pub fn current_limit(&self) -> usize {
245 self.inner
246 .state
247 .lock()
248 .expect("load shedder state lock poisoned")
249 .current_limit
250 }
251
252 pub fn in_flight(&self) -> usize {
253 self.inner.in_flight.load(Ordering::Acquire)
254 }
255
256 pub fn snapshot(&self) -> LoadShedderSnapshot {
257 let now = Instant::now();
258 let mut state = self
259 .inner
260 .state
261 .lock()
262 .expect("load shedder state lock poisoned");
263 rotate_buckets(&mut state, &self.inner.config, now);
264 let (maximum_throughput, minimum_latency) = rolling_capacity(&state, &self.inner.config);
265 LoadShedderSnapshot {
266 in_flight: self.in_flight(),
267 smoothed_in_flight: state.smoothed_in_flight,
268 current_limit: state.current_limit,
269 maximum_throughput,
270 minimum_latency,
271 cooling_down: state.cooldown_until.is_some_and(|until| now < until),
272 }
273 }
274}
275
276pub struct ShedPermit {
278 inner: Arc<Inner>,
279 started_at: Instant,
280}
281
282impl Drop for ShedPermit {
283 fn drop(&mut self) {
284 let active = self
285 .inner
286 .in_flight
287 .fetch_sub(1, Ordering::Release)
288 .saturating_sub(1);
289 let elapsed = self.started_at.elapsed();
290 let now = Instant::now();
291 let mut state = self
292 .inner
293 .state
294 .lock()
295 .expect("load shedder state lock poisoned");
296
297 if self.inner.config.mode == LoadShedderMode::CpuThroughput {
298 rotate_buckets(&mut state, &self.inner.config, now);
299 let bucket = state
300 .buckets
301 .back_mut()
302 .expect("shedder always has a bucket");
303 bucket.completed = bucket.completed.saturating_add(1);
304 bucket.minimum_latency = Some(
305 bucket
306 .minimum_latency
307 .map_or(elapsed, |old| old.min(elapsed)),
308 );
309 let alpha = self.inner.config.in_flight_smoothing;
310 state.smoothed_in_flight =
311 alpha * active as f64 + (1.0 - alpha) * state.smoothed_in_flight;
312 state.current_limit = production_limit(&state, &self.inner.config);
313 return;
314 }
315
316 state.sample_count += 1;
317 state.total_latency += elapsed;
318 if state.sample_count < self.inner.config.sample_window {
319 return;
320 }
321 let average_latency = state.total_latency / state.sample_count as u32;
322 if average_latency > self.inner.config.target_latency {
323 state.current_limit = (state.current_limit * 9 / 10).max(1);
324 } else {
325 state.current_limit = (state.current_limit + 1).min(self.inner.config.max_concurrency);
326 }
327 state.sample_count = 0;
328 state.total_latency = Duration::ZERO;
329 }
330}
331
332fn rotate_buckets(state: &mut ShedderState, config: &LoadShedderConfig, now: Instant) {
333 let latest = state
334 .buckets
335 .back()
336 .expect("shedder always has a bucket")
337 .started_at;
338 let elapsed = now.saturating_duration_since(latest);
339 let steps = (elapsed.as_nanos() / config.bucket_duration.as_nanos()) as usize;
340 for step in 0..steps.min(config.bucket_count) {
341 state.buckets.push_back(Bucket {
342 started_at: latest + config.bucket_duration * (step as u32 + 1),
343 completed: 0,
344 minimum_latency: None,
345 });
346 }
347 while state.buckets.len() > config.bucket_count {
348 state.buckets.pop_front();
349 }
350 if steps >= config.bucket_count {
351 state.buckets.clear();
352 state.buckets.push_back(Bucket {
353 started_at: now,
354 completed: 0,
355 minimum_latency: None,
356 });
357 }
358}
359
360fn rolling_capacity(state: &ShedderState, config: &LoadShedderConfig) -> (f64, Option<Duration>) {
361 let maximum = state
362 .buckets
363 .iter()
364 .map(|bucket| bucket.completed)
365 .max()
366 .unwrap_or(0);
367 let throughput = maximum as f64 / config.bucket_duration.as_secs_f64();
368 let minimum_latency = state
369 .buckets
370 .iter()
371 .filter_map(|bucket| bucket.minimum_latency)
372 .min();
373 (throughput, minimum_latency)
374}
375
376fn production_limit(state: &ShedderState, config: &LoadShedderConfig) -> usize {
377 let (throughput, minimum_latency) = rolling_capacity(state, config);
378 let Some(minimum_latency) = minimum_latency else {
379 return config.max_concurrency;
380 };
381 ((throughput * minimum_latency.as_secs_f64()).ceil().max(1.0) as usize)
382 .min(config.max_concurrency)
383}
384
385struct ProcessCpuSource {
386 state: Mutex<ProcessCpuState>,
387}
388
389struct ProcessCpuState {
390 wall: Instant,
391 cpu: Duration,
392 usage: f64,
393}
394
395impl ProcessCpuSource {
396 fn new() -> Self {
397 Self {
398 state: Mutex::new(ProcessCpuState {
399 wall: Instant::now(),
400 cpu: process_cpu_time(),
401 usage: 0.0,
402 }),
403 }
404 }
405}
406
407impl CpuSource for ProcessCpuSource {
408 fn usage(&self) -> f64 {
409 let now = Instant::now();
410 let mut state = self.state.lock().expect("CPU sampler lock poisoned");
411 let wall = now.saturating_duration_since(state.wall);
412 if wall < Duration::from_millis(100) {
413 return state.usage;
414 }
415 let cpu = process_cpu_time();
416 let used = cpu.saturating_sub(state.cpu).as_secs_f64();
417 let cores = std::thread::available_parallelism().map_or(1, usize::from) as f64;
418 state.usage = (used / wall.as_secs_f64() / cores).clamp(0.0, 1.0);
419 state.wall = now;
420 state.cpu = cpu;
421 state.usage
422 }
423}
424
425#[cfg(unix)]
426fn process_cpu_time() -> Duration {
427 let mut usage = std::mem::MaybeUninit::<libc::rusage>::zeroed();
428 if unsafe { libc::getrusage(libc::RUSAGE_SELF, usage.as_mut_ptr()) } != 0 {
429 return Duration::ZERO;
430 }
431 let usage = unsafe { usage.assume_init() };
432 let seconds = (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec).max(0) as u64;
433 let micros = (usage.ru_utime.tv_usec + usage.ru_stime.tv_usec).max(0) as u64;
434 Duration::from_secs(seconds) + Duration::from_micros(micros)
435}
436
437#[cfg(not(unix))]
438fn process_cpu_time() -> Duration {
439 Duration::ZERO
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445 use std::{
446 sync::atomic::{AtomicU64, Ordering},
447 thread,
448 };
449
450 struct TestCpu(AtomicU64);
451 impl TestCpu {
452 fn new(usage: f64) -> Self {
453 Self(AtomicU64::new(usage.to_bits()))
454 }
455 fn set(&self, usage: f64) {
456 self.0.store(usage.to_bits(), Ordering::Release);
457 }
458 }
459 impl CpuSource for TestCpu {
460 fn usage(&self) -> f64 {
461 f64::from_bits(self.0.load(Ordering::Acquire))
462 }
463 }
464
465 #[test]
466 fn rejects_work_after_reaching_the_limit() {
467 let shedder = AdaptiveShedder::new(LoadShedderConfig::new(1, Duration::from_secs(1)));
468 let permit = shedder.try_acquire().expect("first request is admitted");
469 assert!(shedder.try_acquire().is_none());
470 assert_eq!(shedder.in_flight(), 1);
471 drop(permit);
472 assert_eq!(shedder.in_flight(), 0);
473 }
474
475 #[test]
476 fn reduces_the_limit_when_latency_exceeds_the_target() {
477 let shedder = AdaptiveShedder::new(
478 LoadShedderConfig::new(10, Duration::from_millis(1)).with_sample_window(2),
479 );
480 for _ in 0..2 {
481 let permit = shedder.try_acquire().unwrap();
482 thread::sleep(Duration::from_millis(2));
483 drop(permit);
484 }
485 assert!(shedder.current_limit() < 10);
486 }
487
488 #[test]
489 fn production_mode_sheds_during_cpu_saturation_and_recovers_after_cooldown() {
490 let cpu = Arc::new(TestCpu::new(0.1));
491 let config = LoadShedderConfig::production(8)
492 .with_cpu_threshold(0.8)
493 .with_rolling_window(Duration::from_millis(10), 4)
494 .with_cooldown(Duration::from_millis(15))
495 .with_in_flight_smoothing(1.0);
496 let shedder = AdaptiveShedder::with_cpu_source(config, cpu.clone());
497
498 let warmup = shedder.try_acquire().unwrap();
499 thread::sleep(Duration::from_millis(2));
500 drop(warmup);
501 let active = shedder.try_acquire().unwrap();
502 cpu.set(0.95);
503 assert!(
504 shedder.try_acquire().is_none(),
505 "CPU pressure should reject above learned capacity"
506 );
507 cpu.set(0.1);
508 assert!(
509 shedder.try_acquire().is_none(),
510 "cooldown should prevent an immediate surge"
511 );
512 thread::sleep(Duration::from_millis(20));
513 assert!(
514 shedder.try_acquire().is_some(),
515 "traffic should recover after cooldown"
516 );
517 drop(active);
518 }
519
520 #[test]
521 fn production_mode_admits_sparse_traffic_and_completes_concurrent_permits() {
522 let cpu = Arc::new(TestCpu::new(1.0));
523 let config = LoadShedderConfig::production(4).with_in_flight_smoothing(1.0);
524 let shedder = AdaptiveShedder::with_cpu_source(config, cpu);
525 let permit = shedder
526 .try_acquire()
527 .expect("sparse traffic has no learned overload ceiling");
528 drop(permit);
529 assert_eq!(shedder.in_flight(), 0);
530
531 let shedder = AdaptiveShedder::new(LoadShedderConfig::production(16));
532 let threads: Vec<_> = (0..8)
533 .map(|_| {
534 let shedder = shedder.clone();
535 thread::spawn(move || drop(shedder.try_acquire().unwrap()))
536 })
537 .collect();
538 for thread in threads {
539 thread.join().unwrap();
540 }
541 assert_eq!(shedder.in_flight(), 0);
542 assert_eq!(shedder.snapshot().maximum_throughput, 8.0);
543 }
544}