1pub mod budget;
5pub mod cgroup;
6
7pub use budget::{BudgetInputs, DEFAULT_CAP, detect_cpu_budget, resolve_workers};
8
9use std::sync::{Arc, Condvar, Mutex};
10
11pub const MEMORY_FLOOR: u64 = 256 * 1024 * 1024;
13
14pub fn effective_memory_limit(
22 available: u64,
23 cgroup_v2: Option<u64>,
24 cgroup_v1: Option<u64>,
25) -> u64 {
26 let mut smallest = available;
27 if let Some(v) = cgroup_v2 {
28 smallest = smallest.min(v);
29 }
30 if let Some(v) = cgroup_v1 {
31 smallest = smallest.min(v);
32 }
33 (smallest / 4).max(MEMORY_FLOOR)
34}
35
36pub fn default_memory_limit() -> u64 {
43 #[cfg(target_os = "linux")]
44 {
45 let available = if let Ok(s) = std::fs::read_to_string("/proc/meminfo") {
46 cgroup::parse_meminfo_available(&s)
47 } else {
48 None
49 };
50
51 if let Some(avail) = available {
52 let v2 = {
54 let mut limit = None;
55 if let Ok(cgroup_contents) = std::fs::read_to_string("/proc/self/cgroup")
56 && let Some(path) = cgroup::parse_self_cgroup_v2_path(&cgroup_contents)
57 {
58 let nested_path = format!("/sys/fs/cgroup{}/memory.max", path);
59 if let Ok(s) = std::fs::read_to_string(&nested_path) {
60 limit = cgroup::parse_cgroup_v2_memory_max(&s);
61 }
62 }
63 limit.or_else(|| {
64 std::fs::read_to_string("/sys/fs/cgroup/memory.max")
65 .ok()
66 .and_then(|s| cgroup::parse_cgroup_v2_memory_max(&s))
67 })
68 };
69
70 let v1 = std::fs::read_to_string("/sys/fs/cgroup/memory/memory.limit_in_bytes")
73 .ok()
74 .and_then(|s| cgroup::parse_cgroup_v1_memory_limit(&s));
75
76 return effective_memory_limit(avail, v2, v1);
77 }
78 }
79 MEMORY_FLOOR
80}
81
82#[derive(Default)]
83struct GovState {
84 workers_out: usize,
85 bytes_out: u64,
86}
87
88pub struct Governor {
95 workers: usize,
96 memory_limit: u64,
97 state: Mutex<GovState>,
98 cv: Condvar,
99}
100
101impl Governor {
102 pub fn new(workers: usize, memory_limit: u64) -> Arc<Self> {
103 Arc::new(Self {
104 workers: workers.max(1),
105 memory_limit,
106 state: Mutex::new(GovState::default()),
107 cv: Condvar::new(),
108 })
109 }
110
111 pub fn from_inputs(inputs: &BudgetInputs, memory_limit: u64) -> Arc<Self> {
112 Self::new(resolve_workers(inputs), memory_limit)
113 }
114
115 pub fn workers(&self) -> usize {
116 self.workers
117 }
118
119 pub fn memory_limit(&self) -> u64 {
120 self.memory_limit
121 }
122
123 pub fn outstanding(&self) -> usize {
124 self.state
125 .lock()
126 .expect("governor mutex poisoned")
127 .workers_out
128 }
129
130 pub fn reserved_bytes(&self) -> u64 {
131 self.state
132 .lock()
133 .expect("governor mutex poisoned")
134 .bytes_out
135 }
136
137 pub fn acquire_many(self: &Arc<Self>, want: usize, per_worker_bytes: u64) -> LeaseSet {
160 let want = want.max(1);
161 let mut st = self.state.lock().expect("governor mutex poisoned");
162 loop {
163 let cpu_free = self.workers.saturating_sub(st.workers_out);
164 if cpu_free > 0 {
165 let mem_free = self.memory_limit.saturating_sub(st.bytes_out);
166 let by_mem = match mem_free.checked_div(per_worker_bytes) {
169 Some(n) => n as usize,
170 None => want,
171 };
172 let grant = want.min(cpu_free).min(by_mem).max(1);
174 let bytes = per_worker_bytes.saturating_mul(grant as u64);
175 st.workers_out += grant;
176 st.bytes_out = st.bytes_out.saturating_add(bytes);
177 return LeaseSet {
178 gov: Arc::clone(self),
179 workers: grant,
180 bytes,
181 };
182 }
183 st = self.cv.wait(st).expect("governor mutex poisoned");
184 }
185 }
186
187 pub fn acquire(self: &Arc<Self>) -> LeaseSet {
189 self.acquire_many(1, 0)
190 }
191
192 pub fn try_acquire(self: &Arc<Self>) -> Option<LeaseSet> {
194 let mut st = self.state.lock().expect("governor mutex poisoned");
195 if st.workers_out >= self.workers {
196 return None;
197 }
198 st.workers_out += 1;
199 Some(LeaseSet {
200 gov: Arc::clone(self),
201 workers: 1,
202 bytes: 0,
203 })
204 }
205
206 pub fn workers_for(&self, per_worker_bytes: u64) -> usize {
212 if per_worker_bytes == 0 {
213 return self.workers;
214 }
215 let by_memory = (self.memory_limit / per_worker_bytes) as usize;
216 by_memory.clamp(1, self.workers)
217 }
218}
219
220pub struct LeaseSet {
222 gov: Arc<Governor>,
223 workers: usize,
224 bytes: u64,
225}
226
227impl LeaseSet {
228 pub fn workers(&self) -> usize {
229 self.workers
230 }
231
232 pub fn bytes(&self) -> u64 {
233 self.bytes
234 }
235}
236
237impl Drop for LeaseSet {
238 fn drop(&mut self) {
239 let mut st = self.gov.state.lock().expect("governor mutex poisoned");
240 st.workers_out = st.workers_out.saturating_sub(self.workers);
241 st.bytes_out = st.bytes_out.saturating_sub(self.bytes);
242 self.gov.cv.notify_all();
245 }
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251 use std::sync::atomic::{AtomicUsize, Ordering};
252
253 const GIB: u64 = 1024 * 1024 * 1024;
254
255 #[test]
256 fn leases_are_bounded_by_the_worker_count() {
257 let g = Governor::new(2, GIB);
258 let _a = g.try_acquire().expect("first lease");
259 let _b = g.try_acquire().expect("second lease");
260 assert!(g.try_acquire().is_none(), "third lease must not be issued");
261 assert_eq!(g.outstanding(), 2);
262 }
263
264 #[test]
265 fn dropping_a_lease_returns_it_to_the_pool() {
266 let g = Governor::new(1, GIB);
267 {
268 let _a = g.try_acquire().expect("lease");
269 assert!(g.try_acquire().is_none());
270 }
271 assert_eq!(g.outstanding(), 0);
272 assert!(
273 g.try_acquire().is_some(),
274 "lease must be reusable after drop"
275 );
276 }
277
278 #[test]
279 fn nested_parallelism_cannot_exceed_the_budget() {
280 let g = Governor::new(4, GIB);
283 let peak = Arc::new(AtomicUsize::new(0));
284 let live = Arc::new(AtomicUsize::new(0));
285
286 std::thread::scope(|s| {
287 for _outer in 0..8 {
288 let (g, peak, live) = (Arc::clone(&g), Arc::clone(&peak), Arc::clone(&live));
289 s.spawn(move || {
290 for _inner in 0..8 {
291 let _lease = g.acquire();
292 let now = live.fetch_add(1, Ordering::SeqCst) + 1;
293 peak.fetch_max(now, Ordering::SeqCst);
294 std::thread::yield_now();
295 live.fetch_sub(1, Ordering::SeqCst);
296 }
297 });
298 }
299 });
300
301 assert!(
302 peak.load(Ordering::SeqCst) <= 4,
303 "peak was {}",
304 peak.load(Ordering::SeqCst)
305 );
306 assert!(peak.load(Ordering::SeqCst) >= 1);
307 assert_eq!(g.outstanding(), 0, "every lease must be returned");
308 }
309
310 #[test]
311 fn acquire_blocks_until_a_lease_frees_rather_than_failing() {
312 let g = Governor::new(1, GIB);
313 let held = g.try_acquire().expect("lease");
314 let g2 = Arc::clone(&g);
315 let h = std::thread::spawn(move || {
316 let _l = g2.acquire();
317 true
318 });
319 std::thread::sleep(std::time::Duration::from_millis(250));
320 assert!(
321 !h.is_finished(),
322 "acquire must block while the pool is full"
323 );
324 drop(held);
325 assert!(h.join().unwrap());
326 }
327
328 #[test]
329 fn memory_clamp_reduces_workers_rather_than_oom_ing() {
330 let g = Governor::new(8, 2 * GIB);
332 assert_eq!(g.workers_for(700 * 1024 * 1024), 2);
333 }
334
335 #[test]
336 fn memory_clamp_never_reports_fewer_than_one_worker() {
337 let g = Governor::new(8, 100);
339 assert_eq!(g.workers_for(GIB), 1);
340 }
341
342 #[test]
343 fn zero_demand_codecs_get_the_full_budget() {
344 let g = Governor::new(8, GIB);
345 assert_eq!(g.workers_for(0), 8);
346 }
347
348 #[test]
349 fn memory_clamp_never_exceeds_the_cpu_budget() {
350 let g = Governor::new(2, 1024 * GIB);
351 assert_eq!(
352 g.workers_for(1024),
353 2,
354 "plenty of RAM does not buy more CPUs"
355 );
356 }
357
358 #[test]
359 fn from_inputs_applies_the_conservative_default() {
360 let i = BudgetInputs {
361 detected: 16,
362 ..Default::default()
363 };
364 assert_eq!(Governor::from_inputs(&i, GIB).workers(), 8);
365 }
366
367 #[test]
368 fn a_small_container_limit_is_clamped_up_to_the_floor() {
369 let host = 64 * 1024 * 1024 * 1024;
371 assert_eq!(
372 effective_memory_limit(host, Some(512 * 1024 * 1024), None),
373 MEMORY_FLOOR
374 );
375 }
376
377 #[test]
378 fn a_container_limit_beats_a_larger_host_reading() {
379 let host = 64 * 1024 * 1024 * 1024;
384 let container = 8 * 1024 * 1024 * 1024;
385 assert_eq!(
386 effective_memory_limit(host, Some(container), None),
387 2 * 1024 * 1024 * 1024,
388 "8 GiB container -> 2 GiB budget, not the 16 GiB the host would give"
389 );
390 }
391
392 #[test]
393 fn with_no_cgroup_limit_the_host_reading_stands() {
394 let host = 64 * 1024 * 1024 * 1024;
395 assert_eq!(
396 effective_memory_limit(host, None, None),
397 16 * 1024 * 1024 * 1024
398 );
399 }
400
401 #[test]
402 fn the_floor_applies_after_every_other_clamp() {
403 assert_eq!(effective_memory_limit(1024, Some(2048), None), MEMORY_FLOOR);
404 }
405
406 #[test]
407 fn default_memory_limit_is_sane() {
408 let m = default_memory_limit();
409 assert!(m >= 256 * 1024 * 1024, "floor keeps small hosts usable");
410 }
411
412 #[test]
413 fn acquire_many_grants_what_is_available_rather_than_blocking_for_all() {
414 let g = Governor::new(4, GIB);
415 let a = g.acquire_many(3, 0);
416 assert_eq!(a.workers(), 3);
417 let b = g.acquire_many(3, 0);
419 assert_eq!(b.workers(), 1);
420 assert_eq!(g.outstanding(), 4);
421 }
422
423 #[test]
424 fn acquire_many_reserves_memory_so_two_callers_cannot_both_take_the_budget() {
425 let g = Governor::new(8, 2 * GIB);
429 let a = g.acquire_many(2, 700 * 1024 * 1024);
430 assert_eq!(a.workers(), 2);
431
432 let b = g.acquire_many(2, 700 * 1024 * 1024);
435 assert_eq!(b.workers(), 1, "only one worker's memory remains");
436
437 assert_eq!(g.reserved_bytes(), 3 * 700 * 1024 * 1024);
442
443 assert!(g.reserved_bytes() <= g.memory_limit() + (g.workers() as u64) * 700 * 1024 * 1024);
446 }
447
448 #[test]
449 fn a_lease_set_releases_both_resources_on_drop() {
450 let g = Governor::new(4, GIB);
451 {
452 let _a = g.acquire_many(4, 100);
453 assert_eq!(g.outstanding(), 4);
454 assert_eq!(g.reserved_bytes(), 400);
455 }
456 assert_eq!(g.outstanding(), 0);
457 assert_eq!(g.reserved_bytes(), 0);
458 }
459
460 #[test]
461 fn one_worker_is_granted_even_when_memory_alone_would_allow_none() {
462 let g = Governor::new(8, 100);
465 let a = g.acquire_many(4, GIB);
466 assert_eq!(a.workers(), 1);
467 }
468
469 #[test]
470 fn acquire_many_blocks_only_while_no_worker_slot_is_free() {
471 let g = Governor::new(1, GIB);
472 let held = g.acquire_many(1, 0);
473 let g2 = Arc::clone(&g);
474 let h = std::thread::spawn(move || g2.acquire_many(4, 0).workers());
475 std::thread::sleep(std::time::Duration::from_millis(250));
476 assert!(!h.is_finished(), "must wait while the pool is empty");
477 drop(held);
478 assert_eq!(h.join().unwrap(), 1);
479 }
480
481 #[test]
482 fn single_shot_acquisition_does_not_deadlock_under_nested_demand() {
483 let g = Governor::new(4, GIB);
487 std::thread::scope(|s| {
488 for _ in 0..8 {
489 let g = Arc::clone(&g);
490 s.spawn(move || {
491 let set = g.acquire_many(3, 0);
492 assert!(set.workers() >= 1 && set.workers() <= 4);
493 std::thread::yield_now();
494 });
495 }
496 });
497 assert_eq!(g.outstanding(), 0);
498 }
499}