Skip to main content

rust_zero_core/
load.rs

1use std::{
2    collections::VecDeque,
3    sync::{
4        atomic::{AtomicUsize, Ordering},
5        Arc, Mutex,
6    },
7    time::{Duration, Instant},
8};
9
10/// Admission policy used by [`AdaptiveShedder`].
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LoadShedderMode {
13    /// Adjust a concurrency ceiling from batches of observed response times.
14    Latency,
15    /// Shed only during CPU pressure, using recent throughput and response-time capacity.
16    CpuThroughput,
17}
18
19/// Settings for adaptive admission control.
20#[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    /// Creates the original latency-adaptive limiter.
35    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    /// Creates a CPU- and throughput-aware production shedder.
59    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/// Current production-shedder measurements.
139#[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/// Dynamically sheds requests when latency or process load indicates saturation.
150#[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    /// Attempts to reserve execution capacity. Dropping a successful permit records its latency.
184    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
276/// A capacity reservation from [`AdaptiveShedder`].
277pub 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}