Skip to main content

runnerd_core/api/
admission.rs

1//! How a runner was admitted, as the job history keeps it for its job: the policy, what the runner reserved of
2//! the slice's budget and what that went by, and the budget and the slice when it started. Sizes are bytes, times
3//! seconds since the Unix epoch.
4
5use std::fmt::Write as _;
6
7use serde::{Deserialize, Serialize};
8
9use crate::config::ByteSize;
10
11/// `[host] admission`, as a job's record and the budget name it.
12#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "kebab-case")]
14pub enum AdmissionPolicy {
15    /// Every runner reserves its memory cap.
16    #[default]
17    Caps,
18    /// Every runner reserves its predicted peak.
19    Measured,
20}
21
22/// What a runner reserved goes by, in the order measured admission falls back.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "kebab-case", tag = "kind")]
25pub enum Basis {
26    /// The job's own runs, at least `[sizing] min_jobs` of them: their p95 peak plus the margin.
27    Job {
28        /// How many.
29        runs: u64,
30    },
31    /// Too few runs of the job but enough of its profile: the larger of the profile's p95 peak and the job's own
32    /// largest, plus the margin.
33    Profile {
34        /// How many runs of the profile.
35        runs: u64,
36    },
37    /// No run of the job yet but enough of its profile: the profile's largest peak plus the margin, since a new
38    /// job may be its heaviest.
39    ProfileMax {
40        /// How many runs of the profile.
41        runs: u64,
42    },
43    /// A run of the job went past what it reserved lately: the job's largest peak plus the margin.
44    Overran {
45        /// When the job is predicted from its p95 again.
46        until: u64,
47    },
48    /// The cap: admission by caps, or too few runs of the job and of its profile.
49    Cap,
50    /// The cap: a run of the job peaked at its cap or was killed for memory lately.
51    Unpredictable {
52        /// When the job may be predicted again.
53        until: u64,
54    },
55    /// The cap: measured admission had no recent sample of the slice's memory to check.
56    Unmeasured,
57}
58
59/// How a job's runner was admitted.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
61pub struct Admission {
62    /// The policy it was admitted by.
63    pub policy: AdmissionPolicy,
64    /// What it reserved of the budget: under measured admission the largest prediction of its repository's
65    /// queued jobs, since GitHub may have given it any of them.
66    pub reserved: u64,
67    /// What that went by.
68    pub basis: Basis,
69    /// The budget the other runners, the mirror and any shell left before it started; `None` for an unlimited
70    /// slice.
71    pub free: Option<u64>,
72    /// The slice's memory then, without page cache; `None` without a recent sample.
73    pub slice: Option<u64>,
74}
75
76impl Admission {
77    /// One line on how the runner was admitted, as `runnerctl job` and the dashboard say it, such as `admitted by
78    /// measured peaks: reserved 1.25G, the p95 peak of 10 runs of its job + the margin; 10G of the budget was free,
79    /// the slice at 2G`.
80    pub fn line(&self) -> String {
81        let size = |bytes: u64| ByteSize(bytes).to_string();
82        let reserved = size(self.reserved);
83        let why = match self.basis {
84            Basis::Job { runs } => {
85                format!("reserved {reserved}, the p95 peak of {runs} runs of its job + the margin")
86            }
87            Basis::Profile { runs } => format!(
88                "reserved {reserved}, from {runs} runs of its profile: its job had too few of its own"
89            ),
90            Basis::ProfileMax { runs } => format!(
91                "reserved {reserved}, the largest peak of {runs} runs of its profile + the margin: its job had no \
92                 runs of its own"
93            ),
94            Basis::Overran { .. } => format!(
95                "reserved {reserved}, its job's largest peak + the margin: a run of it went past what it reserved \
96                 lately"
97            ),
98            Basis::Cap if self.policy == AdmissionPolicy::Caps => {
99                format!("reserved its {reserved} cap")
100            }
101            Basis::Cap => format!("reserved its {reserved} cap: too few runs to predict from"),
102            Basis::Unpredictable { .. } => format!(
103                "reserved its {reserved} cap: a run of its job peaked at its cap or was killed for memory lately"
104            ),
105            Basis::Unmeasured => {
106                format!("reserved its {reserved} cap: no recent sample of the slice's memory")
107            }
108        };
109        let policy = match self.policy {
110            AdmissionPolicy::Caps => "caps",
111            AdmissionPolicy::Measured => "measured peaks",
112        };
113        let mut line = format!("admitted by {policy}: {why}");
114        if let Some(free) = self.free {
115            let _ = write!(line, "; {} of the budget was free", size(free));
116        }
117        if let Some(slice) = self.slice {
118            let _ = write!(line, ", the slice at {}", size(slice));
119        }
120        line
121    }
122}