Skip to main content

runnerd_core/admission/
host.rs

1//! What the host's memory leaves runners beside its other programs (ADR 0055): the slice's runners may hold what
2//! they hold now without page cache plus the host's `MemAvailable`, less a margin kept for the host's page cache, so
3//! reclaim stays cheap; and nothing starts while the host, short of memory, stalls for it. Each bound has
4//! hysteresis, so a host at the edge does not start and hold runners by turns. Pure: the engine passes in the
5//! reading and what it remembers of the last plan.
6
7use crate::api::{HostBudget, HostMemory};
8
9/// Starts stop once every runnable task of the host waits for memory this share of the last 10 s, in hundredths of
10/// a percent: 5%, while the host is short of memory (`stalls`).
11pub const STALL_ENTER: u32 = 500;
12
13/// Once stopped, starts resume when that share falls under this: 1%.
14pub const STALL_LEAVE: u32 = 100;
15
16/// The margin kept free at least: 2G.
17const MARGIN_MIN: u64 = 2 << 30;
18
19/// The margin as a share of the host's memory, as a divisor: a sixteenth, 4G of 64G.
20const MARGIN_SHARE: u64 = 16;
21
22/// What admission remembers of the host from the last plan.
23#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
24pub struct Remembered {
25    /// A job waited for the host's memory, or the host stalled: the margin grows by half until one fits again with
26    /// it. The engine clears it while no runner runs, so the first runner into an empty slice needs only the margin.
27    pub short: bool,
28    /// The host stalled: starts resume only under `STALL_LEAVE`.
29    pub stalled: bool,
30}
31
32/// The margin `MemAvailable` keeps beside the runners' predicted peaks for a host with `total` memory: the larger of
33/// `MARGIN_MIN` and `total / MARGIN_SHARE`, half again as much while the host is `short`.
34pub fn margin(total: u64, short: bool) -> u64 {
35    let margin = MARGIN_MIN.max(total / MARGIN_SHARE);
36    if short {
37        margin.saturating_add(margin / 2)
38    } else {
39        margin
40    }
41}
42
43/// Whether `memory`'s stall shares say the host stalls for memory, given whether it did at the last plan. A stall
44/// counts only while `MemAvailable` is under twice the margin: the shares are the whole host's, and a runner
45/// reclaiming at its own cap raises them with memory to spare, which starts elsewhere do not make worse.
46pub fn stalls(memory: &HostMemory, before: bool) -> bool {
47    let full = memory.full.map_or(0, |full| full.avg10);
48    let stalling = full >= STALL_ENTER || (before && full >= STALL_LEAVE);
49    stalling && memory.available < margin(memory.total, false).saturating_mul(2)
50}
51
52/// What `memory` lets the slice's runners hold in all: what the slice holds now without page cache plus
53/// `MemAvailable`, less the margin, larger while the host is `short`.
54pub fn limit(memory: &HostMemory, short: bool) -> u64 {
55    memory
56        .slice
57        .saturating_add(memory.available)
58        .saturating_sub(margin(memory.total, short))
59}
60
61/// What `memory` leaves runners beside the `reserved` memory of what runs in the slice, given what the last plan
62/// `remembered`.
63pub fn budget(memory: HostMemory, reserved: u64, remembered: Remembered) -> HostBudget {
64    let stalled = stalls(&memory, remembered.stalled);
65    let limit = limit(&memory, remembered.short || stalled);
66    HostBudget {
67        memory,
68        limit,
69        spare: u64::try_from(left(limit, reserved, stalled)).unwrap_or(0),
70        stalled,
71    }
72}
73
74/// What a host `limit` leaves new runners beside the `reserved` memory of what runs in the slice: negative by what
75/// they reserve past it, none while the host is `stalled`. A runner that stops gives its reservation back to this,
76/// so a deficit stays owed rather than hidden by `HostBudget::spare`, which stops at 0.
77pub fn left(limit: u64, reserved: u64, stalled: bool) -> i64 {
78    if stalled {
79        return 0;
80    }
81    let bytes = |bytes: u64| i64::try_from(bytes).unwrap_or(i64::MAX);
82    bytes(limit).saturating_sub(bytes(reserved))
83}