Skip to main content

decdn_node/load_shed/
resource_pressure.rs

1//! The `ResourcePressure` load-shed policy: hysteresis over node-wide
2//! concurrency, a hard egress ceiling, hit-before-miss priority, and a
3//! per-client fair-share cap that engages only under pressure.
4
5use std::sync::atomic::{AtomicBool, Ordering};
6
7use super::{LoadShedPolicy, PressureSnapshot, RequestClass, ShedDecision, ShedReason};
8
9/// Tunable thresholds, resolved from node config (see `ResolvedLoadShed`).
10#[derive(Debug, Clone, Copy)]
11pub struct Params {
12    /// Measured egress ceiling in bytes/sec. `0` disables the egress gate.
13    pub egress_budget_bps: u64,
14    /// Concurrency high-water mark: at or above it, the node becomes pressured.
15    pub max_serves_high: u32,
16    /// Concurrency low-water mark: at or below it, pressure clears (hysteresis).
17    pub max_serves_low: u32,
18    /// Per-client concurrent-serve ceiling, enforced ONLY while pressured.
19    /// `0` disables per-client fairness.
20    pub per_client_cap: u32,
21}
22
23/// Sheds the miss tier under concurrency pressure and any class under egress
24/// saturation, while capping a single client's share only during real
25/// contention. Holds one bit of hysteresis state.
26#[derive(Debug)]
27pub struct ResourcePressure {
28    params: Params,
29    /// Latched pressure state: set at the high-water mark, cleared at the
30    /// low-water mark, so the node does not flap at a single boundary.
31    pressured: AtomicBool,
32}
33
34impl ResourcePressure {
35    /// A policy with these thresholds, starting unpressured.
36    #[must_use]
37    pub const fn new(params: Params) -> Self {
38        Self {
39            params,
40            pressured: AtomicBool::new(false),
41        }
42    }
43
44    /// Update and read the hysteresis latch from the live concurrency count.
45    fn update_pressure(&self, streams_in_flight: u32) -> bool {
46        if streams_in_flight >= self.params.max_serves_high {
47            self.pressured.store(true, Ordering::Relaxed);
48        } else if streams_in_flight <= self.params.max_serves_low {
49            self.pressured.store(false, Ordering::Relaxed);
50        }
51        self.pressured.load(Ordering::Relaxed)
52    }
53}
54
55impl LoadShedPolicy for ResourcePressure {
56    fn decide(&self, class: RequestClass, snap: &PressureSnapshot) -> ShedDecision {
57        let pressured = self.update_pressure(snap.streams_in_flight);
58
59        // Per-client fairness engages ONLY under pressure (fairness invariant):
60        // a client is capped for holding more than its share during real
61        // contention, never to reserve a slot for a client that might arrive.
62        if pressured
63            && self.params.per_client_cap > 0
64            && snap.client_streams_in_flight >= self.params.per_client_cap
65        {
66            return ShedDecision::Shed(ShedReason::ClientAtCapacity);
67        }
68
69        // Hard egress ceiling: when the pipe is full, admitting another stream
70        // — hit or miss — creates no bandwidth, so shed it. `0` disables.
71        if self.params.egress_budget_bps > 0 && snap.egress_bps >= self.params.egress_budget_bps {
72            return ShedDecision::Shed(ShedReason::EgressSaturated);
73        }
74
75        // Concurrency pressure sheds the miss tier first (it fronts upstream
76        // cost and stresses disk/CPU); hits ride until egress is the constraint.
77        if pressured && class == RequestClass::CacheMiss {
78            return ShedDecision::Shed(ShedReason::NodeAtCapacity);
79        }
80
81        ShedDecision::Admit
82    }
83
84    fn pressure_active(&self) -> bool {
85        self.pressured.load(Ordering::Relaxed)
86    }
87}
88
89#[cfg(test)]
90mod tests;