use std::collections::BTreeSet;
use isoloom_manifest::{Manifest, Part, Placeholder, RunVar};
use crate::Result;
use crate::error::Error;
use crate::name::RunName;
use crate::state::RunState;
use crate::store::RunRecord;
const LAST: u8 = 99;
pub(crate) fn needed(manifest: &Manifest) -> bool {
manifest
.templates()
.iter()
.any(|(_, t)| t.parts().iter().any(|p| matches!(p, Part::Placeholder(Placeholder::Run(RunVar::Slot)))))
}
pub(crate) fn taken(records: &[RunRecord], me: &RunName, place: Option<&str>) -> BTreeSet<u8> {
records
.iter()
.filter(|r| &r.name != me && r.host_label.as_deref() == place && r.state != RunState::Absent)
.filter_map(|r| r.slot)
.collect()
}
pub(crate) fn pick(own: Option<u8>, taken: &BTreeSet<u8>, place: Option<&str>) -> Result<u8> {
if let Some(slot) = own.filter(|s| (1..=LAST).contains(s) && !taken.contains(s)) {
return Ok(slot);
}
(1..=LAST).find(|s| !taken.contains(s)).ok_or_else(|| Error::SlotsFull {
place: place.unwrap_or("this host").to_owned(),
held: taken.len(),
})
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn record(name: &str, place: Option<&str>, slot: Option<u8>, state: RunState) -> RunRecord {
let mut r = RunRecord::fresh(
RunName::new(name).unwrap(),
"/l/run.json".into(),
String::new(),
isoloom_manifest::Target::Proxmox,
isoloom_manifest::Driver::Terraform,
);
r.host_label = place.map(String::from);
r.slot = slot;
r.state = state;
r
}
#[test]
fn a_run_gets_the_lowest_slot_the_others_on_its_place_dont_hold() {
let me = RunName::new("b").unwrap();
let records = [
record("a", Some("pve"), Some(1), RunState::Ready),
record("c", Some("pve"), Some(3), RunState::Failed),
record("d", Some("pve"), Some(2), RunState::Absent),
record("e", Some("other"), Some(2), RunState::Ready),
record("b", Some("pve"), Some(1), RunState::Absent),
];
let taken = taken(&records, &me, Some("pve"));
assert_eq!(taken, BTreeSet::from([1, 3]));
assert_eq!(pick(None, &taken, Some("pve")).unwrap(), 2);
assert_eq!(pick(Some(5), &taken, Some("pve")).unwrap(), 5, "a run keeps its own");
assert_eq!(pick(Some(1), &taken, Some("pve")).unwrap(), 2, "unless another took it since");
}
#[test]
fn a_full_place_is_refused_with_its_name() {
let all: BTreeSet<u8> = (1..=LAST).collect();
let e = pick(None, &all, Some("pve")).unwrap_err();
assert_eq!(e.code(), crate::ErrorCode::SlotsFull);
assert_eq!(e.to_string(), "no slot is free on `pve`: 99 runs hold one; stop one first");
}
proptest! {
#[test]
fn a_picked_slot_is_free_and_in_range(taken in proptest::collection::btree_set(1u8..=99, 0..98), own in proptest::option::of(0u8..=120)) {
let s = pick(own, &taken, None).unwrap();
prop_assert!((1..=LAST).contains(&s) && !taken.contains(&s));
}
}
}