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}