1use super::{ByteSize, Config, ConfigError};
6use crate::api::{AdmissionPolicy, Check, CheckStatus, ConfigCheck, Tone};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Verdict {
11 Fits,
13 NoBudget,
15 DoesNotFit,
17 Invalid,
20}
21
22impl Verdict {
23 pub fn label(self) -> &'static str {
25 match self {
26 Verdict::Fits => "Fits the budget",
27 Verdict::NoBudget => "Valid, with no MemoryMax to fit",
28 Verdict::DoesNotFit => "Does not fit",
29 Verdict::Invalid => "Not valid",
30 }
31 }
32
33 pub fn tone(self) -> Tone {
35 match self {
36 Verdict::Fits => Tone::Good,
37 Verdict::NoBudget => Tone::Warn,
38 Verdict::DoesNotFit | Verdict::Invalid => Tone::Bad,
39 }
40 }
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct CheckBudget {
46 pub admission: AdmissionPolicy,
48 pub needed: u64,
50 pub memory_max: Option<u64>,
52 pub memory_caps: Option<u64>,
54}
55
56impl CheckBudget {
57 pub fn needed_label(&self) -> &'static str {
59 match self.admission {
60 AdmissionPolicy::Caps => "Memory caps",
61 AdmissionPolicy::Measured => "Largest runner + mirror",
62 }
63 }
64
65 pub fn over(&self) -> bool {
67 self.memory_max.is_some_and(|max| self.needed > max)
68 }
69
70 pub fn remainder(&self) -> Option<Remainder> {
72 let max = self.memory_max?;
73 Some(if self.over() {
74 Remainder {
75 label: "Over by",
76 memory: self.needed - max,
77 }
78 } else {
79 Remainder {
80 label: "Left",
81 memory: max - self.needed,
82 }
83 })
84 }
85
86 pub fn admission_note(&self) -> Option<String> {
89 if self.admission != AdmissionPolicy::Measured {
90 return None;
91 }
92 let mut note = "Admission is measured: runners reserve their predicted peaks, and slots only bound how many \
93 run at once."
94 .to_owned();
95 if let Some(caps) = self.memory_caps {
96 note = format!(
97 "{note} Every slot at its cap at once would take {}.",
98 ByteSize(caps)
99 );
100 }
101 Some(note)
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct Remainder {
108 pub label: &'static str,
110 pub memory: u64,
112}
113
114impl ConfigCheck {
115 pub fn verdict(&self, parsed: Option<&Config>) -> Verdict {
119 if self.valid {
120 return if self.memory_max.is_none() {
121 Verdict::NoBudget
122 } else {
123 Verdict::Fits
124 };
125 }
126 let over = match parsed {
127 Some(config) => matches!(
128 config.validate(self.memory_max.map(ByteSize)),
129 Err(ConfigError::OverBudget { .. } | ConfigError::ProfileOverBudget { .. })
130 ),
131 None => self.budget(None).is_some_and(|budget| budget.over()),
132 };
133 if over {
134 Verdict::DoesNotFit
135 } else {
136 Verdict::Invalid
137 }
138 }
139
140 pub fn budget(&self, parsed: Option<&Config>) -> Option<CheckBudget> {
143 let (admission, needed) = match (self.memory_needed, parsed) {
144 (Some(needed), _) => (self.admission, needed),
145 (None, Some(config)) => (
146 config.host.admission.policy(),
147 config.memory_needed().ok()?.0,
148 ),
149 (None, None) if self.admission == AdmissionPolicy::Caps => {
150 (self.admission, self.memory_caps?)
151 }
152 (None, None) => return None,
153 };
154 Some(CheckBudget {
155 admission,
156 needed,
157 memory_max: self.memory_max,
158 memory_caps: self.memory_caps,
159 })
160 }
161
162 pub fn doctor_check(&self) -> Check {
165 if !self.valid {
166 return Check {
167 name: "config".to_owned(),
168 status: CheckStatus::Fail,
169 detail: self.message.clone(),
170 fix: Some("change runnerd.toml or the slice's limits".to_owned()),
171 };
172 }
173 let detail = if self.warnings.is_empty() {
174 self.message.clone()
175 } else {
176 format!(
177 "{}, with warnings: {}",
178 self.message,
179 self.warnings.join("; ")
180 )
181 };
182 Check {
183 name: "config".to_owned(),
184 status: CheckStatus::Pass,
185 detail,
186 fix: None,
187 }
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct BudgetBlock {
194 pub label: String,
196 pub memory: u64,
198 pub profile: Option<usize>,
200}
201
202impl Config {
203 pub fn budget_blocks(&self, admission: AdmissionPolicy) -> Option<Vec<BudgetBlock>> {
207 let profiles = self
208 .profiles
209 .iter()
210 .enumerate()
211 .map(|(index, (name, profile))| (index, name, profile));
212 let mut blocks: Vec<BudgetBlock> = match admission {
213 AdmissionPolicy::Caps => profiles
214 .map(|(index, name, profile)| {
215 Some(BudgetBlock {
216 label: format!("{name} ×{}", profile.slots),
217 memory: profile.memory.0.checked_mul(u64::from(profile.slots))?,
218 profile: Some(index),
219 })
220 })
221 .collect::<Option<_>>()?,
222 AdmissionPolicy::Measured => profiles
223 .max_by_key(|(_, _, profile)| profile.memory.0)
224 .map(|(index, name, profile)| BudgetBlock {
225 label: format!("{name} ×1"),
226 memory: profile.memory.0,
227 profile: Some(index),
228 })
229 .into_iter()
230 .collect(),
231 };
232 if self.caches.mirror.enabled {
233 blocks.push(BudgetBlock {
234 label: "mirror".to_owned(),
235 memory: self.mirror_memory(),
236 profile: None,
237 });
238 }
239 let total = blocks
240 .iter()
241 .try_fold(0u64, |total, block| total.checked_add(block.memory));
242 total.map(|_| blocks)
243 }
244
245 pub fn check_against(&self, memory_max: Option<ByteSize>) -> ConfigCheck {
248 let figures = self
249 .validate(memory_max)
250 .and_then(|warnings| Ok((warnings, self.memory_caps()?, self.memory_needed()?)));
251 match figures {
252 Ok((warnings, total, needed)) => {
253 let message = match (self.host.admission, memory_max) {
254 (super::Admission::Measured, Some(max)) => format!(
255 "valid under admission = \"measured\": the largest runner with the mirror needs {needed} \
256 of the {max} MemoryMax; every slot at its cap at once would take {total}"
257 ),
258 _ => format!("valid, memory caps sum to {total}"),
259 };
260 ConfigCheck {
261 valid: true,
262 message,
263 warnings: warnings.iter().map(ToString::to_string).collect(),
264 admission: self.host.admission.policy(),
265 memory_caps: Some(total.0),
266 memory_needed: Some(needed.0),
267 memory_max: memory_max.map(|max| max.0),
268 revision: None,
269 }
270 }
271 Err(error) => invalid(error.to_string(), Some(self), memory_max),
272 }
273 }
274}
275
276pub(super) fn invalid(
278 message: String,
279 config: Option<&Config>,
280 max: Option<ByteSize>,
281) -> ConfigCheck {
282 ConfigCheck {
283 valid: false,
284 message,
285 warnings: Vec::new(),
286 admission: config
287 .map(|config| config.host.admission.policy())
288 .unwrap_or_default(),
289 memory_caps: config
290 .and_then(|config| config.memory_caps().ok())
291 .map(|caps| caps.0),
292 memory_needed: config
293 .and_then(|config| config.memory_needed().ok())
294 .map(|need| need.0),
295 memory_max: max.map(|max| max.0),
296 revision: None,
297 }
298}
299
300#[cfg(test)]
301mod tests;