use std::collections::BTreeSet;
use crate::admission::Key;
use crate::admission::recent::{self, Counted, Ending, JobRuns, RECENT_RUNS, Recent, SIZING_TIMES};
use crate::api::{self, Peaks, Resize, RevertCause, SizingRevert, job_title};
use crate::config::{Admission, Config};
use super::{Capped, History, MemorySet, Ran, spread};
pub const HISTORY: u64 = 30 * 24 * 3600 * 1000;
pub const CHANGES_READ: u64 = 2 * HISTORY;
pub const FAILED_RUNS: usize = 3;
pub(super) fn windows(config: &Config, history: &History, now: u64) -> Recent {
let size = SIZING_TIMES.saturating_mul(RECENT_RUNS.max(u64::from(config.sizing.min_jobs)));
let mut recent = Recent::new(size);
let start = now.saturating_sub(HISTORY);
for (run, ending) in recent::rows(history) {
if run.queued_at < start {
continue;
}
recent.read(
run,
&run.profile,
ending,
cap_of(config, run),
recent::finished(run),
);
}
recent
}
fn cap_of(config: &Config, run: &Ran) -> u64 {
run.memory_cap
.or_else(|| config.profiles.get(&run.profile).map(|p| p.memory.0))
.unwrap_or(u64::MAX)
}
pub(super) fn peaks<'run>(runs: impl Iterator<Item = &'run Counted>, on_cap: u64) -> Peaks {
let runs: Vec<&Counted> = runs.collect();
Peaks {
jobs: runs.len() as u64,
at_cap: runs.iter().filter(|run| run.capped.met()).count() as u64,
on_cap,
memory: spread(runs.iter().map(|run| run.peak).collect()),
cpu: spread(runs.iter().map(|run| run.cpu_peak).collect()),
}
}
fn run_need(run: &Counted, margin: u32) -> u64 {
if run.capped.met() {
api::need(run.peak.max(run.cap), margin)
} else {
api::need(run.peak, margin).min(run.cap)
}
}
pub(super) fn floor(runs: &JobRuns, margin: u32) -> u64 {
runs.window()
.map(|run| run_need(run, margin))
.max()
.unwrap_or(0)
}
fn larger_caps(runs: &JobRuns, margin: u32, above: u64) -> u64 {
runs.window()
.filter(|run| run.cap > above && run.capped != Capped::Assumed)
.map(|run| run_need(run, margin).min(run.cap))
.max()
.unwrap_or(0)
}
pub(super) fn on_cap(runs: &JobRuns, now: u64) -> bool {
runs.held(now, None).is_some()
}
pub(super) fn took(set: &[MemorySet], profile: &str, memory: u64, now: u64) -> u64 {
let mut since = None;
let mut seen = false;
for event in set.iter().filter(|event| event.profile == profile) {
seen = true;
if event.memory != memory {
since = None;
} else if since.is_none() {
since = Some(event.at);
}
}
since.unwrap_or(if seen { now } else { 0 })
}
pub(super) fn raise_need(
history: &History,
recent: &Recent,
profile: &str,
cap: u64,
took: u64,
margin: u32,
key: Option<&Key>,
) -> u64 {
history
.jobs
.iter()
.chain(&history.cancelled)
.filter(|run| {
run.profile == profile
&& run.memory_cap == Some(cap)
&& recent::finished(run) >= took
&& run.capped(cap) == Capped::Met
&& key.is_none_or(|key| Key::of(run) == *key)
})
.map(|run| {
let larger = recent
.job(&Key::of(run), profile)
.map_or(0, |runs| larger_caps(runs, margin, cap));
api::need(run.memory_peak.max(cap), margin).max(larger)
})
.max()
.unwrap_or(0)
}
fn too_far<'change>(
history: &'change History,
profile: &str,
) -> impl Iterator<Item = (&'change api::SizingChange, SizingRevert)> {
history
.changes
.iter()
.filter(move |change| change.profile == profile && change.to.memory < change.from.memory)
.filter_map(move |change| {
cause(history, profile, change).map(|(job, cause)| {
(
change,
SizingRevert {
from: change.to.memory,
to: change.from.memory,
job,
cause,
},
)
})
})
}
fn cause(
history: &History,
profile: &str,
lowering: &api::SizingChange,
) -> Option<(String, RevertCause)> {
let (lowered, from) = (lowering.to.memory, lowering.from.memory);
let since = lowering.at.saturating_mul(1000);
let after: Vec<(&Ran, Ending)> = recent::rows(history)
.into_iter()
.filter(|(run, _)| {
run.profile == profile
&& run.memory_cap == Some(lowered)
&& recent::finished(run) >= since
})
.collect();
let named = |run: &Ran| {
format!(
"{} {}",
run.repository,
job_title(run.workflow.as_deref(), &run.name, None)
)
};
if let Some((run, _)) = after
.iter()
.find(|(run, _)| run.capped(lowered) == Capped::Met)
{
return Some((named(run), RevertCause::Met));
}
let jobs: BTreeSet<Key> = after.iter().map(|(run, _)| Key::of(run)).collect();
for key in jobs {
let succeeded_at_from = history.jobs.iter().any(|run| {
run.profile == profile
&& Key::of(run) == key
&& run.succeeded
&& run.memory_cap == Some(from)
});
if !succeeded_at_from {
continue;
}
let mut row: Vec<(u64, Option<&str>)> = Vec::new();
for (run, ending) in after.iter().filter(|(run, _)| Key::of(run) == key) {
if *ending == Ending::Cancelled {
continue;
}
if run.succeeded {
row.clear();
} else if run.failed && !row.iter().any(|(id, _)| *id == run.run_id) {
row.push((run.run_id, run.branch.as_deref()));
}
let branches: BTreeSet<Option<&str>> = row.iter().map(|(_, branch)| *branch).collect();
if row.len() >= FAILED_RUNS && branches.len() >= 2 {
return Some((
named(run),
RevertCause::Failed {
runs: u32::try_from(row.len()).unwrap_or(u32::MAX),
},
));
}
}
}
None
}
pub(super) fn revert(
config: &Config,
history: &History,
profile: &str,
current: Resize,
) -> Option<(SizingRevert, Resize)> {
let last = history
.changes
.iter()
.rfind(|change| change.profile == profile && change.to.memory != change.from.memory)?;
if last.to.memory != current.memory || last.to.memory >= last.from.memory {
return None;
}
let since = last.at.saturating_mul(1000);
let mut events = history
.memory_set
.iter()
.filter(|event| event.profile == profile && event.at >= since)
.peekable();
events.peek()?;
if events.any(|event| event.memory != current.memory) {
return None;
}
let (_, revert) = too_far(history, profile).find(|(change, _)| *change == last)?;
let slots = match config.host.admission {
Admission::Caps => last.from.slots,
Admission::Measured => current.slots,
};
Some((
revert,
Resize {
memory: last.from.memory,
slots,
cpus: None,
},
))
}
pub(super) fn revert_floor(history: &History, profile: &str) -> u64 {
too_far(history, profile)
.map(|(_, revert)| revert.to)
.max()
.unwrap_or(0)
}