agentd/runtime/
pressure.rs1use std::path::PathBuf;
24use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
28pub enum Level {
29 Ok = 0,
30 Warn = 1,
32 Shed = 2,
34}
35
36impl Level {
37 pub fn as_str(self) -> &'static str {
38 match self {
39 Level::Ok => "ok",
40 Level::Warn => "warn",
41 Level::Shed => "shed",
42 }
43 }
44 fn from_u8(v: u8) -> Level {
45 match v {
46 2 => Level::Shed,
47 1 => Level::Warn,
48 _ => Level::Ok,
49 }
50 }
51}
52
53pub struct Pressure {
54 disk_path: Option<PathBuf>,
59 shed_below: u64,
61 level: AtomicU8,
62 cause: AtomicU8,
64 last_check_ms: AtomicU64,
65 pub disk_free: AtomicU64,
67}
68
69const RECHECK_MS: u64 = 2_000;
70
71impl Pressure {
72 pub fn refusal(&self, low_priority: bool) -> Option<String> {
77 match self.level() {
78 Level::Shed => Some(format!(
79 "{} pressure (shedding new work; in-flight work drains)",
80 self.cause()
81 )),
82 Level::Warn if low_priority => Some(format!(
83 "{} pressure (low-priority work sheds at warn)",
84 self.cause()
85 )),
86 _ => None,
87 }
88 }
89}
90
91impl Pressure {
92 pub fn new(disk_path: Option<PathBuf>, shed_below: u64) -> Pressure {
93 Pressure {
94 disk_path,
95 shed_below,
96 level: AtomicU8::new(0),
97 cause: AtomicU8::new(0),
98 last_check_ms: AtomicU64::new(0),
99 disk_free: AtomicU64::new(u64::MAX),
100 }
101 }
102
103 pub fn level(&self) -> Level {
105 let now = crate::state::now_ms();
106 let last = self.last_check_ms.load(Ordering::Relaxed);
107 if now.saturating_sub(last) >= RECHECK_MS
108 && self
109 .last_check_ms
110 .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
111 .is_ok()
112 {
113 let (level, cause) = self.assess();
114 self.level.store(level as u8, Ordering::Relaxed);
115 self.cause.store(cause, Ordering::Relaxed);
116 }
117 Level::from_u8(self.level.load(Ordering::Relaxed))
118 }
119
120 pub fn shedding(&self) -> bool {
122 self.level() == Level::Shed
123 }
124
125 pub fn cause(&self) -> &'static str {
127 match self.cause.load(Ordering::Relaxed) {
128 1 => "disk",
129 2 => "memory",
130 _ => "none",
131 }
132 }
133
134 fn assess(&self) -> (Level, u8) {
135 if let Some(p) = &self.disk_path
136 && self.shed_below > 0
137 && let Some(free) = free_bytes(p)
138 {
139 self.disk_free.store(free, Ordering::Relaxed);
140 if free < self.shed_below {
141 return (Level::Shed, 1);
142 }
143 if free < self.shed_below.saturating_mul(2) {
144 return (Level::Warn, 1);
145 }
146 }
147 if crate::supervisor::cgroup::under_memory_pressure() {
148 return (Level::Shed, 2);
149 }
150 (Level::Ok, 0)
151 }
152}
153
154pub fn free_bytes(path: &std::path::Path) -> Option<u64> {
160 use std::os::unix::ffi::OsStrExt;
161 let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
162 let mut sv: libc::statvfs = unsafe { std::mem::zeroed() };
163 if unsafe { libc::statvfs(c.as_ptr(), &mut sv) } != 0 {
164 return None;
165 }
166 Some((sv.f_bavail as u64).saturating_mul(sv.f_frsize as u64))
167}
168
169pub fn parse_bytes(s: &str) -> Result<u64, String> {
174 let t = s.trim();
175 if t.is_empty() {
176 return Err("empty size".into());
177 }
178 let lower = t.to_ascii_lowercase();
179 let (num, mult) = if let Some(n) = lower
180 .strip_suffix("gib")
181 .or_else(|| lower.strip_suffix("gb"))
182 .or_else(|| lower.strip_suffix('g'))
183 {
184 (n, 1u64 << 30)
185 } else if let Some(n) = lower
186 .strip_suffix("mib")
187 .or_else(|| lower.strip_suffix("mb"))
188 .or_else(|| lower.strip_suffix('m'))
189 {
190 (n, 1u64 << 20)
191 } else if let Some(n) = lower
192 .strip_suffix("kib")
193 .or_else(|| lower.strip_suffix("kb"))
194 .or_else(|| lower.strip_suffix('k'))
195 {
196 (n, 1u64 << 10)
197 } else {
198 (lower.as_str(), 1u64)
199 };
200 let v: f64 = num
201 .trim()
202 .parse()
203 .map_err(|_| format!("invalid size {s:?} (want e.g. 256MB, 1.5GiB, or bytes)"))?;
204 if !(v.is_finite() && v >= 0.0) {
205 return Err(format!("invalid size {s:?}"));
206 }
207 Ok((v * mult as f64) as u64)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn refusal_gives_low_priority_its_teeth_one_level_early() {
216 let free = free_bytes(std::path::Path::new("/")).expect("statvfs /");
220 let warn_band = Pressure::new(Some("/".into()), free * 2 / 3);
221 assert_eq!(warn_band.level(), Level::Warn);
222 assert!(
223 warn_band.refusal(false).is_none(),
224 "normal work admits at warn"
225 );
226 let msg = warn_band.refusal(true).expect("low sheds at warn");
227 assert!(msg.contains("low-priority"), "{msg}");
228
229 let shedding = Pressure::new(Some("/".into()), u64::MAX);
231 assert_eq!(shedding.level(), Level::Shed);
232 assert!(shedding.refusal(false).is_some());
233 assert!(shedding.refusal(true).is_some());
234
235 let none = Pressure::new(None, 0);
237 assert_eq!(none.level(), Level::Ok);
238 assert!(none.refusal(true).is_none());
239 }
240
241 #[test]
242 fn sizes_parse_and_the_root_filesystem_reports_headroom() {
243 assert_eq!(parse_bytes("256MB").unwrap(), 256 << 20);
244 assert_eq!(parse_bytes("256MiB").unwrap(), 256 << 20);
245 assert_eq!(
246 parse_bytes("1.5GiB").unwrap(),
247 (1.5 * (1u64 << 30) as f64) as u64
248 );
249 assert_eq!(parse_bytes("1024").unwrap(), 1024);
250 assert_eq!(parse_bytes("0").unwrap(), 0);
251 assert!(parse_bytes("lots").is_err());
252 assert!(free_bytes(std::path::Path::new("/")).unwrap() > 0);
254 }
255
256 #[test]
258 fn levels_change_at_the_declared_thresholds() {
259 let p = Pressure::new(None, 256 << 20);
260 assert_eq!(p.assess().0, Level::Ok);
263 }
264}