feat(#2289): push-based server-warning registry with RAII guard
This commit is contained in:
parent
d01d6d91e2
commit
9f98925c14
5 changed files with 361 additions and 34 deletions
|
|
@ -8,10 +8,12 @@
|
|||
//! operator *before* an ENOSPC, not after.
|
||||
//!
|
||||
//! [`server_warnings`] is the public surface: it returns a flat list of
|
||||
//! [`ServerWarning`]s for `/api/state`. The dashboard renders whatever it
|
||||
//! returns, coloured by `level`, so adding a new system warning (memory
|
||||
//! pressure, a failed unit, …) is a backend-only change — no frontend
|
||||
//! edit. Keep producers cheap; this runs on every `/api/state` assembly.
|
||||
//! [`ServerWarning`]s for `/api/state`. It is now just a snapshot of the
|
||||
//! push-based [`crate::warnings`] registry — producers raise/clear their
|
||||
//! own warnings via an RAII guard rather than being re-probed on every
|
||||
//! render. The disk-pressure check is reworked onto that model here:
|
||||
//! [`refresh_disk_warning`] drives a held guard from a periodic task (see
|
||||
//! `spawn` in `main.rs`) instead of a `statvfs` per `/api/state`.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
@ -37,36 +39,62 @@ pub struct ServerWarning {
|
|||
const DISK_WARN_PCT: f64 = 85.0;
|
||||
const DISK_CRIT_PCT: f64 = 95.0;
|
||||
|
||||
/// Collect the current server-level warnings for the dashboard banner.
|
||||
/// Each producer pushes zero or more [`ServerWarning`]s; the frontend
|
||||
/// renders whatever this returns. Cheap to call on every `/api/state`
|
||||
/// assembly (currently a single `statvfs`).
|
||||
/// Snapshot the current server-level warnings for the dashboard banner —
|
||||
/// a cheap read of the push-based [`crate::warnings`] registry. Producers
|
||||
/// (the disk watch below, background converge sweeps, …) raise and clear
|
||||
/// their own entries via [`crate::warnings::set_warning`]; nothing is
|
||||
/// re-probed here.
|
||||
#[must_use]
|
||||
pub fn server_warnings() -> Vec<ServerWarning> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(d) = nix_disk_usage()
|
||||
&& d.used_pct >= DISK_WARN_PCT
|
||||
{
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
reason = "byte counts stay well under f64's 2^53 exact-integer range, so this GiB conversion loses no precision"
|
||||
)]
|
||||
let free_gib = d.free_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
out.push(ServerWarning {
|
||||
kind: "disk_pressure",
|
||||
level: if d.used_pct >= DISK_CRIT_PCT {
|
||||
"crit"
|
||||
} else {
|
||||
"warn"
|
||||
},
|
||||
message: format!(
|
||||
"host nix store {:.0}% full ({free_gib:.1} GiB free) \
|
||||
— garbage-collect the store before it runs out of space",
|
||||
d.used_pct
|
||||
),
|
||||
});
|
||||
crate::warnings::snapshot()
|
||||
}
|
||||
|
||||
/// Pure disk-pressure decision: given usage, return the banner
|
||||
/// `(level, message)` or `None` when below the warn threshold. Split from
|
||||
/// the probe + guard plumbing so the threshold + formatting is unit-tested
|
||||
/// without a real `statvfs`.
|
||||
fn disk_pressure_warning(used_pct: f64, free_bytes: u64) -> Option<(&'static str, String)> {
|
||||
if used_pct < DISK_WARN_PCT {
|
||||
return None;
|
||||
}
|
||||
#[allow(
|
||||
clippy::cast_precision_loss,
|
||||
reason = "byte counts stay well under f64's 2^53 exact-integer range, so this GiB conversion loses no precision"
|
||||
)]
|
||||
let free_gib = free_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let level = if used_pct >= DISK_CRIT_PCT {
|
||||
"crit"
|
||||
} else {
|
||||
"warn"
|
||||
};
|
||||
let message = format!(
|
||||
"host nix store {used_pct:.0}% full ({free_gib:.1} GiB free) \
|
||||
— garbage-collect the store before it runs out of space"
|
||||
);
|
||||
Some((level, message))
|
||||
}
|
||||
|
||||
/// One tick of the disk-pressure watch: probe the store and reconcile the
|
||||
/// held warning `guard` — raise or refresh it while usage is over
|
||||
/// threshold, drop it (clearing the banner) once back under. Driven by the
|
||||
/// periodic disk-watch task spawned in `main.rs`, replacing the old
|
||||
/// per-render `statvfs` in [`server_warnings`].
|
||||
pub fn refresh_disk_warning(guard: &mut Option<crate::warnings::WarningGuard>) {
|
||||
let decision = nix_disk_usage().and_then(|d| disk_pressure_warning(d.used_pct, d.free_bytes));
|
||||
match decision {
|
||||
Some((level, message)) => match guard {
|
||||
Some(g) => g.update(level, message),
|
||||
None => {
|
||||
*guard = Some(crate::warnings::set_warning(
|
||||
"disk_pressure",
|
||||
level,
|
||||
message,
|
||||
));
|
||||
}
|
||||
},
|
||||
// Back under threshold (or probe failed) — drop the guard to clear.
|
||||
None => *guard = None,
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Agent-state warnings derived from the live container snapshot the
|
||||
|
|
@ -194,6 +222,25 @@ fn disk_usage(path: &str) -> Option<DiskUsage> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn disk_below_threshold_is_none() {
|
||||
assert!(disk_pressure_warning(84.9, 100 << 30).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disk_warn_band_is_amber() {
|
||||
let (level, msg) = disk_pressure_warning(90.0, 20 << 30).expect("warn");
|
||||
assert_eq!(level, "warn");
|
||||
assert!(msg.contains("90% full"));
|
||||
assert!(msg.contains("20.0 GiB free"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disk_crit_band_is_red() {
|
||||
let (level, _) = disk_pressure_warning(95.0, 1 << 30).expect("crit");
|
||||
assert_eq!(level, "crit");
|
||||
}
|
||||
|
||||
fn cv(name: &str, needs_login: bool) -> ContainerView {
|
||||
ContainerView {
|
||||
name: name.to_owned(),
|
||||
|
|
|
|||
Loading…
Reference in a new issue