use crate::api::{
AdmissionPolicy, Budget, ClosedKind, HostBudget, HostMemory, Job, JobMetrics, JobRunner,
JobState, JobSummary, LimitSource, LogSummary, ProfileStatus, RunnerStatus, SeedMemory,
SlotStatus, Status, Timeline,
};
const G: u64 = 1 << 30;
fn runner(reserved: u64) -> RunnerStatus {
RunnerStatus {
repository: "halyard/ledger".to_owned(),
name: "runnerd-jvm-1".to_owned(),
runner_id: 1,
container: "c1".to_owned(),
busy: true,
started_at: 0,
reserved,
warm: false,
}
}
fn status(admission: AdmissionPolicy, memory_max: Option<u64>) -> Status {
Status {
version: "0.0.0".to_owned(),
shutting_down: false,
budget: Budget {
slice: "ci.slice".to_owned(),
memory_max,
memory_in_use: 8 * G,
admission,
..Budget::default()
},
profiles: vec![ProfileStatus {
name: "jvm".to_owned(),
image: "runnerd-runner:base".to_owned(),
cpus: 4,
memory: 8 * G,
reseeding: false,
slots: vec![
SlotStatus {
slot: 1,
runner: Some(runner(8 * G)),
},
SlotStatus {
slot: 2,
runner: None,
},
],
}],
repositories: vec![],
queued: vec![],
}
}
#[test]
fn a_free_slot_under_caps_is_drawn_but_stays_free_beside_what_runs() {
let status = status(AdmissionPolicy::Caps, Some(24 * G));
let parts = status.budget_parts();
assert_eq!(status.budget.remaining(), Some(16 * G));
assert_eq!(
parts
.slots
.iter()
.map(|part| (part.slot, part.reserved))
.collect::<Vec<_>>(),
[(1, true), (2, false)]
);
let free = parts.free.expect("8G past both slots");
assert_eq!(free.memory, 8 * G);
assert_eq!(
free.reading.line(),
"8G of 24G free past every slot's cap; 16G is free beside what runs"
);
}
#[test]
fn measured_admission_draws_only_runners_so_free_is_what_is_left() {
let parts = status(AdmissionPolicy::Measured, Some(24 * G)).budget_parts();
assert_eq!(parts.slots.len(), 1);
assert_eq!(
parts.free.map(|free| free.reading.line()),
Some("16G of 24G free".to_owned())
);
}
#[test]
fn without_memory_max_the_bar_has_room_for_the_mirror_and_seeds() {
let mut status = status(AdmissionPolicy::Caps, None);
status.budget.mirror_memory = G;
status.budget.seed_memory = vec![SeedMemory {
profile: "jvm".to_owned(),
memory: 8 * G,
}];
let parts = status.budget_parts();
assert_eq!(parts.whole, 25 * G, "two slots, the mirror and a seed");
assert!(parts.free.is_none(), "nothing is free without a MemoryMax");
assert_eq!(
parts
.held
.iter()
.map(super::HeldPart::name)
.collect::<Vec<_>>(),
["mirror", "jvm seed"]
);
}
#[test]
fn every_client_says_the_held_measured_budget_admits_no_runner() {
let mut budget = status(AdmissionPolicy::Measured, Some(24 * G)).budget;
budget.held = true;
assert_eq!(
budget.line(),
"8G of 24G reserved in ci.slice by predicted peaks; the slice is past 90% of its MemoryMax, so no \
runner starts"
);
budget.memory_max = None;
budget.held = false;
assert_eq!(
budget.line(),
"8G reserved in ci.slice by predicted peaks, which has no MemoryMax"
);
}
fn host(limit: u64, stalled: bool) -> HostBudget {
HostBudget {
memory: HostMemory {
at: 0,
total: 64 * G,
available: 20 * G,
slice: 8 * G,
some: None,
full: None,
holders: vec![],
},
limit,
spare: limit.saturating_sub(8 * G),
stalled,
}
}
#[test]
fn free_is_what_the_host_leaves_when_it_leaves_less_than_memory_max() {
let mut status = status(AdmissionPolicy::Measured, Some(46 * G));
status.budget.host = Some(host(13 * G, false));
let parts = status.budget_parts();
assert_eq!(
parts.free.map(|free| (free.memory, free.reading.line())),
Some((
5 * G,
"5G free; the host leaves the slice 13G of its 46G".to_owned()
))
);
assert_eq!(
parts
.closed
.map(|closed| (closed.memory, closed.reading.line())),
Some((
33 * G,
"33G held by the host for its other programs and page cache, which leave the slice 13G"
.to_owned()
))
);
assert_eq!(
status.budget.host_line().as_deref(),
Some("host: 20G of 64G available; leaves the slice 13G of its 46G, so 5G is free")
);
}
#[test]
fn the_host_holds_back_the_end_of_the_bar_past_idle_slots_under_caps() {
let mut status = status(AdmissionPolicy::Caps, Some(24 * G));
status.budget.host = Some(host(20 * G, false));
let parts = status.budget_parts();
assert_eq!(
parts.free.map(|free| free.reading.line()),
Some(
"4G free past every slot's cap; 12G is free beside what runs, as the host leaves the slice 20G of its \
24G"
.to_owned()
)
);
assert_eq!(parts.closed.map(|closed| closed.memory), Some(4 * G));
status.budget.host = Some(host(10 * G, false));
let parts = status.budget_parts();
assert_eq!(parts.free, None, "nothing past the idle slot is free");
assert_eq!(parts.closed.map(|closed| closed.memory), Some(8 * G));
status.budget.host = Some(host(8 * G, false));
assert_eq!(
status.budget.host_line().as_deref(),
Some("host: 20G of 64G available; leaves the slice 8G of its 24G, so nothing is free")
);
}
#[test]
fn a_host_that_leaves_as_much_changes_nothing() {
let plain = status(AdmissionPolicy::Measured, Some(24 * G));
let mut status = plain.clone();
status.budget.host = Some(host(30 * G, false));
let parts = status.budget_parts();
assert_eq!(parts.free, plain.budget_parts().free);
assert_eq!(parts.closed, None);
assert_eq!(status.budget.host_withheld(), None);
}
#[test]
fn nothing_is_free_while_the_host_stalls() {
let mut status = status(AdmissionPolicy::Caps, Some(24 * G));
status.budget.host = Some(host(30 * G, true));
let parts = status.budget_parts();
assert_eq!(parts.free, None, "no runner starts, so nothing is free");
let closed = parts.closed.expect("the room past both slots is closed");
assert_eq!(
(closed.memory, closed.kind, closed.reading.line()),
(
8 * G,
ClosedKind::Stalled,
"8G closed: the host's programs stall waiting for memory, so no runner starts"
.to_owned()
)
);
assert_eq!(
status.budget.host_line().as_deref(),
Some("host: 20G of 64G available; stalling, so no runner starts")
);
}
#[test]
fn nothing_is_free_while_the_slice_is_held_though_the_host_leaves_room() {
let mut status = status(AdmissionPolicy::Measured, Some(46 * G));
status.budget.held = true;
status.budget.host = Some(host(13 * G, false));
let parts = status.budget_parts();
assert_eq!(parts.free, None, "no runner starts, so nothing is free");
let closed = parts.closed.expect("the room past the runner is closed");
assert_eq!(
(closed.memory, closed.kind, closed.reading.line()),
(
38 * G,
ClosedKind::Held,
"38G closed: the slice is past 90% of its MemoryMax, so no runner starts".to_owned()
)
);
assert_eq!(
status.budget.host_line().as_deref(),
Some("host: 20G of 64G available; leaves the slice 13G")
);
}
#[test]
fn without_memory_max_the_host_line_says_what_the_host_leaves_free() {
let mut budget = status(AdmissionPolicy::Measured, None).budget;
budget.host = Some(host(13 * G, false));
assert_eq!(
budget.host_line().as_deref(),
Some("host: 20G of 64G available; leaves the slice 13G, so 5G is free")
);
}
fn job(memory_cap: Option<u64>, cpu_cap: Option<u64>) -> Job {
Job {
summary: JobSummary {
id: 7,
repository: "halyard/ledger".to_owned(),
run_id: 1,
run_attempt: 1,
workflow: None,
name: "build".to_owned(),
branch: None,
url: None,
labels: vec![],
state: JobState::Running,
conclusion: None,
runner: Some(JobRunner {
name: "runnerd-jvm-1".to_owned(),
profile: "jvm".to_owned(),
slot: 1,
memory_cap,
cpu_cap,
}),
timeline: Timeline::default(),
metrics: JobMetrics::default(),
log: LogSummary::default(),
},
steps: vec![],
record: None,
admission: None,
}
}
#[test]
fn a_reload_never_replaces_the_cap_a_running_container_started_with() {
let mut profile = status(AdmissionPolicy::Caps, None).profiles.remove(0);
profile.memory = 4 * G;
profile.cpus = 2;
let limits = job(Some(8 * G), None).limits(Some(&profile));
let memory = limits.memory.expect("a memory limit");
assert_eq!((memory.value, memory.source), (8 * G, LimitSource::Cap));
let cpu = limits.cpu.expect("a CPU limit");
assert_eq!((cpu.value, cpu.source), (2000, LimitSource::ProfileNow));
assert_eq!(
limits.line().as_deref(),
Some("capped at 8G; profile now 2 CPUs")
);
assert_eq!(
job(Some(8 * G), Some(4000)).limits(None).line().as_deref(),
Some("capped at 8G and 4 CPUs")
);
}