decdn_node/load_shed/mod.rs
1//! Load-shedding: content-agnostic overload protection for the paid serve path.
2//!
3//! When the node is saturated it refuses NEW serves — regardless of blob or
4//! price — so in-flight streams stay fast. A refusal reuses the delivery
5//! path's `NotFound`-shaped reject, so the client re-routes (ADR 037 / #1174)
6//! at only a latency cost; it is not slashable and carries no reputation
7//! penalty. The decision is a pluggable [`LoadShedPolicy`]; the node wiring
8//! layer selects the implementation from config, mirroring the ADR 040
9//! eviction/admission policy split.
10
11mod controller;
12mod egress;
13mod resource_pressure;
14mod state;
15
16pub use controller::LoadShedController;
17pub use egress::EgressMeter;
18pub use resource_pressure::{Params as ResourcePressureParams, ResourcePressure};
19pub use state::{ShedSlot, ShedState};
20
21/// Which serve tier a request falls into, resolved where `cache.has` is known.
22/// A hit is local — zero upstream cost, pure margin — so it is shed last; a
23/// miss fronts origin egress / upstream USDC and stresses disk and CPU, so it
24/// is shed first.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum RequestClass {
27 /// The blob is already local: zero upstream cost, so it sheds last.
28 CacheHit,
29 /// The blob must be pulled: origin egress plus upstream USDC, so it
30 /// sheds first.
31 CacheMiss,
32}
33
34/// One immutable read of live node pressure, sampled per admission. New
35/// signals are added as fields; a policy ignores fields it does not read, so
36/// the seam is additive.
37#[derive(Debug, Clone, Copy)]
38pub struct PressureSnapshot {
39 /// Serve streams in flight across all clients.
40 pub streams_in_flight: u32,
41 /// Serve streams in flight for THIS client (fairness axis).
42 pub client_streams_in_flight: u32,
43 /// Rolling measured egress, bytes/sec.
44 pub egress_bps: u64,
45}
46
47/// Why a request was shed. Metrics/observability granularity only — every
48/// reason is `NotFound` on the wire.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum ShedReason {
51 /// Node-wide concurrency is above the high-water mark (sheds the miss tier).
52 NodeAtCapacity,
53 /// Measured egress has reached the configured budget (sheds any class).
54 EgressSaturated,
55 /// This client already holds its fair share while the node is pressured.
56 ClientAtCapacity,
57}
58
59/// What a policy decided about one request.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum ShedDecision {
62 /// Serve it.
63 Admit,
64 /// Refuse it, for this reason. The wire answer is `NotFound` regardless.
65 Shed(ShedReason),
66}
67
68/// A pluggable overload-protection policy. `decide` is pure over the snapshot
69/// so it is unit-testable without a running node; the node wiring layer selects
70/// the implementation from config.
71pub trait LoadShedPolicy: Send + Sync {
72 /// Judge one request against a single pressure sample.
73 fn decide(&self, class: RequestClass, snap: &PressureSnapshot) -> ShedDecision;
74
75 /// Whether the policy currently considers the node pressured. Drives the
76 /// `load_shed_pressure_active` gauge; defaults to `false` for policies with
77 /// no pressure notion (e.g. [`AlwaysAdmit`]).
78 fn pressure_active(&self) -> bool {
79 false
80 }
81}
82
83/// The no-op policy: never sheds. A valid operator choice for a node that wants
84/// no heuristic shedding. It is UNWISE under a real flood — the OS OOM-killer
85/// becomes the only backstop — but physical limits bind regardless, and the
86/// `ConnectionLimiter` task caps still apply beneath it, so the choice stays
87/// available.
88#[derive(Debug, Default, Clone, Copy)]
89pub struct AlwaysAdmit;
90
91impl LoadShedPolicy for AlwaysAdmit {
92 fn decide(&self, _class: RequestClass, _snap: &PressureSnapshot) -> ShedDecision {
93 ShedDecision::Admit
94 }
95}
96
97#[cfg(test)]
98mod tests;