use super::{ByteSize, Config, ConfigError};
use crate::api::{AdmissionPolicy, Check, CheckStatus, ConfigCheck, Tone};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Fits,
NoBudget,
DoesNotFit,
Invalid,
}
impl Verdict {
pub fn label(self) -> &'static str {
match self {
Verdict::Fits => "Fits the budget",
Verdict::NoBudget => "Valid, with no MemoryMax to fit",
Verdict::DoesNotFit => "Does not fit",
Verdict::Invalid => "Not valid",
}
}
pub fn tone(self) -> Tone {
match self {
Verdict::Fits => Tone::Good,
Verdict::NoBudget => Tone::Warn,
Verdict::DoesNotFit | Verdict::Invalid => Tone::Bad,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CheckBudget {
pub admission: AdmissionPolicy,
pub needed: u64,
pub memory_max: Option<u64>,
pub memory_caps: Option<u64>,
}
impl CheckBudget {
pub fn needed_label(&self) -> &'static str {
match self.admission {
AdmissionPolicy::Caps => "Memory caps",
AdmissionPolicy::Measured => "Largest runner + mirror",
}
}
pub fn over(&self) -> bool {
self.memory_max.is_some_and(|max| self.needed > max)
}
pub fn remainder(&self) -> Option<Remainder> {
let max = self.memory_max?;
Some(if self.over() {
Remainder {
label: "Over by",
memory: self.needed - max,
}
} else {
Remainder {
label: "Left",
memory: max - self.needed,
}
})
}
pub fn admission_note(&self) -> Option<String> {
if self.admission != AdmissionPolicy::Measured {
return None;
}
let mut note = "Admission is measured: runners reserve their predicted peaks, and slots only bound how many \
run at once."
.to_owned();
if let Some(caps) = self.memory_caps {
note = format!(
"{note} Every slot at its cap at once would take {}.",
ByteSize(caps)
);
}
Some(note)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Remainder {
pub label: &'static str,
pub memory: u64,
}
impl ConfigCheck {
pub fn verdict(&self, parsed: Option<&Config>) -> Verdict {
if self.valid {
return if self.memory_max.is_none() {
Verdict::NoBudget
} else {
Verdict::Fits
};
}
let over = match parsed {
Some(config) => matches!(
config.validate(self.memory_max.map(ByteSize)),
Err(ConfigError::OverBudget { .. } | ConfigError::ProfileOverBudget { .. })
),
None => self.budget(None).is_some_and(|budget| budget.over()),
};
if over {
Verdict::DoesNotFit
} else {
Verdict::Invalid
}
}
pub fn budget(&self, parsed: Option<&Config>) -> Option<CheckBudget> {
let (admission, needed) = match (self.memory_needed, parsed) {
(Some(needed), _) => (self.admission, needed),
(None, Some(config)) => (
config.host.admission.policy(),
config.memory_needed().ok()?.0,
),
(None, None) if self.admission == AdmissionPolicy::Caps => {
(self.admission, self.memory_caps?)
}
(None, None) => return None,
};
Some(CheckBudget {
admission,
needed,
memory_max: self.memory_max,
memory_caps: self.memory_caps,
})
}
pub fn doctor_check(&self) -> Check {
if !self.valid {
return Check {
name: "config".to_owned(),
status: CheckStatus::Fail,
detail: self.message.clone(),
fix: Some("change runnerd.toml or the slice's limits".to_owned()),
};
}
let detail = if self.warnings.is_empty() {
self.message.clone()
} else {
format!(
"{}, with warnings: {}",
self.message,
self.warnings.join("; ")
)
};
Check {
name: "config".to_owned(),
status: CheckStatus::Pass,
detail,
fix: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BudgetBlock {
pub label: String,
pub memory: u64,
pub profile: Option<usize>,
}
impl Config {
pub fn budget_blocks(&self, admission: AdmissionPolicy) -> Option<Vec<BudgetBlock>> {
let profiles = self
.profiles
.iter()
.enumerate()
.map(|(index, (name, profile))| (index, name, profile));
let mut blocks: Vec<BudgetBlock> = match admission {
AdmissionPolicy::Caps => profiles
.map(|(index, name, profile)| {
Some(BudgetBlock {
label: format!("{name} ×{}", profile.slots),
memory: profile.memory.0.checked_mul(u64::from(profile.slots))?,
profile: Some(index),
})
})
.collect::<Option<_>>()?,
AdmissionPolicy::Measured => profiles
.max_by_key(|(_, _, profile)| profile.memory.0)
.map(|(index, name, profile)| BudgetBlock {
label: format!("{name} ×1"),
memory: profile.memory.0,
profile: Some(index),
})
.into_iter()
.collect(),
};
if self.caches.mirror.enabled {
blocks.push(BudgetBlock {
label: "mirror".to_owned(),
memory: self.mirror_memory(),
profile: None,
});
}
let total = blocks
.iter()
.try_fold(0u64, |total, block| total.checked_add(block.memory));
total.map(|_| blocks)
}
pub fn check_against(&self, memory_max: Option<ByteSize>) -> ConfigCheck {
let figures = self
.validate(memory_max)
.and_then(|warnings| Ok((warnings, self.memory_caps()?, self.memory_needed()?)));
match figures {
Ok((warnings, total, needed)) => {
let message = match (self.host.admission, memory_max) {
(super::Admission::Measured, Some(max)) => format!(
"valid under admission = \"measured\": the largest runner with the mirror needs {needed} \
of the {max} MemoryMax; every slot at its cap at once would take {total}"
),
_ => format!("valid, memory caps sum to {total}"),
};
ConfigCheck {
valid: true,
message,
warnings: warnings.iter().map(ToString::to_string).collect(),
admission: self.host.admission.policy(),
memory_caps: Some(total.0),
memory_needed: Some(needed.0),
memory_max: memory_max.map(|max| max.0),
revision: None,
}
}
Err(error) => invalid(error.to_string(), Some(self), memory_max),
}
}
}
pub(super) fn invalid(
message: String,
config: Option<&Config>,
max: Option<ByteSize>,
) -> ConfigCheck {
ConfigCheck {
valid: false,
message,
warnings: Vec::new(),
admission: config
.map(|config| config.host.admission.policy())
.unwrap_or_default(),
memory_caps: config
.and_then(|config| config.memory_caps().ok())
.map(|caps| caps.0),
memory_needed: config
.and_then(|config| config.memory_needed().ok())
.map(|need| need.0),
memory_max: max.map(|max| max.0),
revision: None,
}
}
#[cfg(test)]
mod tests;