Skip to content

Commit 28e1c3e

Browse files
kixelatedclaude
andauthored
moq-relay: add /health load-shedding endpoint (#1604)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1d389f5 commit 28e1c3e

11 files changed

Lines changed: 905 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 34 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

doc/bin/relay/config.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ cert = "cert.pem"
9696
key = "key.pem"
9797
```
9898

99+
### \[web.health]
100+
101+
Thresholds for the `/health` load-shedding probe (CPU, RAM, network, load
102+
average), or an external `api` to defer the decision to. All keys are optional;
103+
an unset threshold is not enforced, and with none set `/health` is a pure
104+
liveness probe. See [HTTP Endpoints](/bin/relay/http) for the full reference and
105+
value syntax.
106+
107+
```toml
108+
[web.health]
109+
cpu = 75 # percent; `75` or `75%`
110+
ram = "80%" # percent of total, or absolute (`32GB`)
111+
tx = "500MB" # bytes/s; `b` = bits, `B` = bytes (`4Gb`)
112+
load5 = "80%" # load average; raw (`6.0`) or percent of cores; Unix only
113+
```
114+
99115
### \[auth]
100116

101117
Authentication configuration.

doc/bin/relay/http.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,67 @@ curl http://localhost:4443/certificate.sha256
7777
# f4:a3:b2:... (hex-encoded fingerprint)
7878
```
7979

80+
### GET /health
81+
82+
A liveness and load-shedding probe for upstream load balancers.
83+
84+
- Returns `200` with the body `ok` when every configured threshold passes.
85+
- Returns `503` with the body `overloaded`, followed by one line per breached threshold, otherwise.
86+
87+
With no thresholds configured it's a pure liveness probe (always `200`).
88+
Each threshold is independent and only enforced when set, so you can mix and match.
89+
It's unauthenticated so probes don't need a token.
90+
91+
```bash
92+
curl -i http://localhost:4443/health
93+
# HTTP/1.1 503 Service Unavailable
94+
# overloaded
95+
# cpu 82.1% exceeds 75%
96+
# tx 612.0MB/s exceeds 500.0MB/s
97+
```
98+
99+
Thresholds are read from the host via the cross-platform [`sysinfo`](https://crates.io/crates/sysinfo) crate.
100+
Metrics are sampled in the background every `interval` seconds (default 2).
101+
102+
```toml
103+
[web.health]
104+
# Return 503 when global CPU usage exceeds this percentage. Accepts `75` or `75%`.
105+
cpu = 75
106+
107+
# Return 503 when memory usage exceeds a percentage of total RAM (`80%`)
108+
# or an absolute used-bytes amount (`32GB`, `32GiB`).
109+
ram = "80%"
110+
111+
# Return 503 when aggregate received/transmitted throughput exceeds this rate.
112+
# A unit is required; lowercase `b` is bits, uppercase `B` is bytes (`4Gb`, `500MB`).
113+
# `/s` is always implied. Useful for shedding before you saturate the NIC.
114+
rx = "4Gb"
115+
tx = "500MB"
116+
117+
# Return 503 when the load average exceeds these limits. Each accepts a raw
118+
# value (`6.0`) or a percentage of CPU cores (`80%`, i.e. a load of
119+
# `0.8 * cores` — so `100%` is one runnable task per core). Unix only;
120+
# these keys are rejected on Windows (which has no load average).
121+
load1 = "8.0"
122+
load5 = "80%"
123+
load15 = "4.0"
124+
125+
# Seconds between metric samples. Defaults to 2, floored at 1.
126+
interval = 2
127+
128+
# Defer the decision to another service. On each request the relay GETs this
129+
# URL (5s timeout); a non-2xx response or an unreachable service counts as a
130+
# breach (fail closed). Merges with the local thresholds above, so you can use
131+
# it alone to simply proxy the verdict.
132+
api = "http://localhost:9876/health"
133+
```
134+
135+
Every key also has a CLI flag (`--web-health-cpu 75`, `--web-health-ram 80%`,
136+
`--web-health-rx 4Gb`, `--web-health-tx 500MB`, `--web-health-load1 8.0`,
137+
`--web-health-load5 80%`, `--web-health-load15 4.0`, `--web-health-interval 2`,
138+
`--web-health-api http://localhost:9876/health`) and a matching
139+
`MOQ_WEB_HEALTH_*` environment variable.
140+
80141
## See Also
81142

82143
- [Relay Configuration](/bin/relay/config) - Full config reference

rs/moq-relay/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ license = "MIT OR Apache-2.0"
77

88
version = "0.12.6"
99
edition = "2024"
10-
rust-version.workspace = true
10+
# Higher than the workspace MSRV: sysinfo 0.39 uses std's `cfg_select!`, stabilized in Rust 1.95.
11+
# Fine here because moq-relay is a binary, not a published library.
12+
rust-version = "1.95"
1113

1214
keywords = ["quic", "http3", "webtransport", "media", "live"]
1315
categories = ["multimedia", "network-programming", "web-programming"]
@@ -50,6 +52,7 @@ rustls = { version = "0.23", features = ["aws-lc-rs"], default-features = false
5052
serde = { version = "1", features = ["derive"] }
5153
serde_json = "1"
5254
serde_with = { version = "3", features = ["json", "base64"] }
55+
sysinfo = { version = "0.39", default-features = false, features = ["system", "network"] }
5356
thiserror = "2"
5457
tokio = { workspace = true, features = ["full"] }
5558
tokio-rustls = { version = "0.26", default-features = false }

rs/moq-relay/src/cluster.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,10 @@ impl DialMap {
161161
/// unannounce while a timestamp is pending doesn't reset the clock.
162162
fn mark_unannounced(&self, peer: &str, now: Instant) {
163163
let mut map = self.inner.lock().expect("dial map poisoned");
164-
if let Some(entry) = map.get_mut(peer) {
165-
if entry.sources.gossip {
166-
entry.unannounced_at.get_or_insert(now);
167-
}
164+
if let Some(entry) = map.get_mut(peer)
165+
&& entry.sources.gossip
166+
{
167+
entry.unannounced_at.get_or_insert(now);
168168
}
169169
}
170170

rs/moq-relay/src/config.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,44 @@ auth_api = "https://api.moq.dev/cluster/auth"
256256
"TOML's auth.auth_api must not be clobbered by the CLI re-parse",
257257
);
258258
}
259+
260+
/// Same clap+TOML clobber guard for the `[web.health]` thresholds. Each is
261+
/// `Option<T>`, so an absent `--web-health-*` CLI flag must leave the
262+
/// TOML-configured value untouched during the `update_from` re-parse.
263+
static HEALTH_ENV_LOCK: Mutex<()> = Mutex::new(());
264+
265+
#[test]
266+
fn cli_does_not_clobber_toml_health() {
267+
let _guard = HEALTH_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
268+
// SAFETY: HEALTH_ENV_LOCK serializes this with any sibling test touching
269+
// the same env vars.
270+
unsafe {
271+
std::env::remove_var("MOQ_WEB_HEALTH_CPU");
272+
std::env::remove_var("MOQ_WEB_HEALTH_RAM");
273+
}
274+
275+
let toml = r#"
276+
[web.health]
277+
cpu = 75.0
278+
ram = "80%"
279+
"#;
280+
let dir = std::env::temp_dir().join("moq-relay-config-test");
281+
std::fs::create_dir_all(&dir).unwrap();
282+
let path = dir.join("health-toml-wins.toml");
283+
std::fs::write(&path, toml).unwrap();
284+
285+
let args = vec![std::ffi::OsString::from("moq-relay"), std::ffi::OsString::from(&path)];
286+
let config = Config::parse_and_merge(args).expect("config load");
287+
288+
assert_eq!(
289+
config.web.health.cpu,
290+
Some(75.0),
291+
"TOML's web.health.cpu must not be clobbered by the CLI re-parse"
292+
);
293+
assert_eq!(
294+
config.web.health.ram,
295+
Some(crate::MemLimit::Percent(80.0)),
296+
"TOML's web.health.ram must not be clobbered by the CLI re-parse"
297+
);
298+
}
259299
}

0 commit comments

Comments
 (0)