use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use fallow_output::MutantStatus;
use super::{Attempt, ScheduleConfig, ScheduleTimings};
pub(super) struct PhaseDeadline {
at: Instant,
reserve: Option<ConfirmationReserve>,
}
impl PhaseDeadline {
pub(super) fn first_pass(config: &ScheduleConfig, timings: &ScheduleTimings) -> Option<Self> {
let startup = timings.setup + timings.dry_run;
config.deadline.map(|at| Self {
at,
reserve: Some(ConfirmationReserve::new(startup, config.concurrency)),
})
}
pub(super) fn retries(at: Instant) -> Self {
Self { at, reserve: None }
}
pub(super) fn passed(&self) -> bool {
let needed = self
.reserve
.as_ref()
.map_or(Duration::ZERO, ConfirmationReserve::needed);
Instant::now()
.checked_add(needed)
.is_none_or(|end| end >= self.at)
}
pub(super) fn timed<E>(
deadline: Option<&Self>,
retry_extra_ms: u64,
run: impl FnOnce() -> Result<Attempt, E>,
) -> Result<Attempt, E> {
let started = Instant::now();
let attempt = run()?;
if let Some(reserve) = deadline.and_then(|deadline| deadline.reserve.as_ref()) {
reserve.note(attempt.status, started.elapsed(), retry_extra_ms);
}
Ok(attempt)
}
}
pub(super) fn expired(deadline: Option<Instant>) -> bool {
deadline.is_some_and(|at| Instant::now() >= at)
}
struct ConfirmationReserve {
startup_ms: u64,
workers: u64,
suspect_ms: AtomicU64,
suspects: AtomicU64,
run_ms: AtomicU64,
runs: AtomicU64,
}
impl ConfirmationReserve {
fn new(startup: Duration, workers: usize) -> Self {
Self {
startup_ms: duration_ms(startup),
workers: u64::try_from(workers.max(1)).unwrap_or(u64::MAX),
suspect_ms: AtomicU64::new(0),
suspects: AtomicU64::new(0),
run_ms: AtomicU64::new(0),
runs: AtomicU64::new(0),
}
}
fn note(&self, status: MutantStatus, wall: Duration, retry_extra_ms: u64) {
let wall_ms = duration_ms(wall);
self.run_ms.fetch_add(wall_ms, Ordering::SeqCst);
self.runs.fetch_add(1, Ordering::SeqCst);
let retry_ms = match status {
MutantStatus::Survived => wall_ms.saturating_add(retry_extra_ms),
MutantStatus::Timeout => wall_ms.saturating_mul(2),
_ => return,
};
self.suspect_ms.fetch_add(retry_ms, Ordering::SeqCst);
self.suspects.fetch_add(1, Ordering::SeqCst);
}
fn needed(&self) -> Duration {
let runs = self.runs.load(Ordering::SeqCst);
let mean_ms = self
.run_ms
.load(Ordering::SeqCst)
.checked_div(runs)
.unwrap_or(0);
let suspect_ms = self.suspect_ms.load(Ordering::SeqCst);
let startup_ms = if suspect_ms > 0 { self.startup_ms } else { 0 };
let workers = self
.workers
.min(self.suspects.load(Ordering::SeqCst))
.max(1);
Duration::from_millis(
startup_ms
.saturating_add(suspect_ms.div_ceil(workers))
.saturating_add(mean_ms.saturating_mul(2)),
)
}
}
fn duration_ms(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_reserve_is_empty_before_the_first_job() {
let reserve = ConfirmationReserve::new(Duration::from_millis(500), 4);
assert_eq!(reserve.needed(), Duration::ZERO);
}
#[test]
fn a_killed_mutant_reserves_only_the_mean_run_time() {
let reserve = ConfirmationReserve::new(Duration::from_millis(500), 4);
reserve.note(MutantStatus::Killed, Duration::from_millis(100), 0);
assert_eq!(reserve.needed(), Duration::from_millis(200));
}
#[test]
fn a_suspect_reserves_its_retry_and_the_companion_start() {
let reserve = ConfirmationReserve::new(Duration::from_millis(500), 1);
reserve.note(MutantStatus::Survived, Duration::from_millis(100), 0);
reserve.note(MutantStatus::Timeout, Duration::from_millis(100), 0);
assert_eq!(reserve.needed(), Duration::from_millis(1000));
}
#[test]
fn a_survivor_reserves_the_extra_time_of_its_whole_file_retry() {
let reserve = ConfirmationReserve::new(Duration::ZERO, 1);
reserve.note(MutantStatus::Survived, Duration::from_millis(100), 900);
assert_eq!(reserve.needed(), Duration::from_millis(1200));
}
#[test]
fn the_retries_share_no_more_workers_than_suspects() {
let reserve = ConfirmationReserve::new(Duration::ZERO, 4);
reserve.note(MutantStatus::Survived, Duration::from_millis(400), 0);
assert_eq!(reserve.needed(), Duration::from_millis(1200));
reserve.note(MutantStatus::Survived, Duration::from_millis(400), 0);
assert_eq!(reserve.needed(), Duration::from_millis(1200));
}
}