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().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) -> Option<CheckBudget> {
143 self.memory_needed.map(|needed| CheckBudget {
144 admission: self.admission,
145 needed,
146 memory_max: self.memory_max,
147 memory_caps: self.memory_caps,
148 })
149 }
150
151 pub fn doctor_check(&self) -> Check {
154 if !self.valid {
155 return Check {
156 name: "config".to_owned(),
157 status: CheckStatus::Fail,
158 detail: self.message.clone(),
159 fix: Some("change runnerd.toml or the slice's limits".to_owned()),
160 };
161 }
162 let detail = if self.warnings.is_empty() {
163 self.message.clone()
164 } else {
165 format!(
166 "{}, with warnings: {}",
167 self.message,
168 self.warnings.join("; ")
169 )
170 };
171 Check {
172 name: "config".to_owned(),
173 status: CheckStatus::Pass,
174 detail,
175 fix: None,
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct BudgetBlock {
183 pub label: String,
185 pub memory: u64,
187 pub profile: Option<usize>,
189}
190
191impl Config {
192 pub fn budget_blocks(&self, admission: AdmissionPolicy) -> Option<Vec<BudgetBlock>> {
196 let profiles = self
197 .profiles
198 .iter()
199 .enumerate()
200 .map(|(index, (name, profile))| (index, name, profile));
201 let mut blocks: Vec<BudgetBlock> = match admission {
202 AdmissionPolicy::Caps => profiles
203 .map(|(index, name, profile)| {
204 Some(BudgetBlock {
205 label: format!("{name} ×{}", profile.slots),
206 memory: profile.memory.0.checked_mul(u64::from(profile.slots))?,
207 profile: Some(index),
208 })
209 })
210 .collect::<Option<_>>()?,
211 AdmissionPolicy::Measured => profiles
212 .max_by_key(|(_, _, profile)| profile.memory.0)
213 .map(|(index, name, profile)| BudgetBlock {
214 label: format!("{name} ×1"),
215 memory: profile.memory.0,
216 profile: Some(index),
217 })
218 .into_iter()
219 .collect(),
220 };
221 if self.caches.mirror.enabled {
222 blocks.push(BudgetBlock {
223 label: "mirror".to_owned(),
224 memory: self.mirror_memory(),
225 profile: None,
226 });
227 }
228 let total = blocks
229 .iter()
230 .try_fold(0u64, |total, block| total.checked_add(block.memory));
231 total.map(|_| blocks)
232 }
233
234 pub fn check_against(&self, memory_max: Option<ByteSize>) -> ConfigCheck {
237 let figures = self
238 .validate(memory_max)
239 .and_then(|warnings| Ok((warnings, self.memory_caps()?, self.memory_needed()?)));
240 match figures {
241 Ok((warnings, total, needed)) => {
242 let message = match (self.host.admission, memory_max) {
243 (super::Admission::Measured, Some(max)) => format!(
244 "valid under admission = \"measured\": the largest runner with the mirror needs {needed} \
245 of the {max} MemoryMax; every slot at its cap at once would take {total}"
246 ),
247 _ => format!("valid, memory caps sum to {total}"),
248 };
249 ConfigCheck {
250 valid: true,
251 message,
252 warnings: warnings.iter().map(ToString::to_string).collect(),
253 admission: self.host.admission.policy(),
254 memory_caps: Some(total.0),
255 memory_needed: Some(needed.0),
256 memory_max: memory_max.map(|max| max.0),
257 revision: None,
258 }
259 }
260 Err(error) => invalid(error.to_string(), Some(self), memory_max),
261 }
262 }
263}
264
265pub(super) fn invalid(
267 message: String,
268 config: Option<&Config>,
269 max: Option<ByteSize>,
270) -> ConfigCheck {
271 ConfigCheck {
272 valid: false,
273 message,
274 warnings: Vec::new(),
275 admission: config
276 .map(|config| config.host.admission.policy())
277 .unwrap_or_default(),
278 memory_caps: config
279 .and_then(|config| config.memory_caps().ok())
280 .map(|caps| caps.0),
281 memory_needed: config
282 .and_then(|config| config.memory_needed().ok())
283 .map(|need| need.0),
284 memory_max: max.map(|max| max.0),
285 revision: None,
286 }
287}
288
289#[cfg(test)]
290mod tests;