use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::time::MissedTickBehavior;
use tracing::{info, warn};
use crate::core::build_lease::config::BuildLeaseConfig;
use crate::core::build_lease::evict::{SweepOutcome, effective_evict_pct, sweep};
use crate::core::build_lease::slots::SlotDir;
use crate::core::disk_usage_guard;
use crate::daemon::state::DaemonState;
pub(crate) const ENV_ENABLED: &str = "TRUSTY_MPM_SLOT_POOL_EVICT";
pub(crate) const ENV_INTERVAL_SECS: &str = "TRUSTY_MPM_SLOT_POOL_EVICT_INTERVAL_SECS";
pub(crate) const DEFAULT_INTERVAL_SECS: u64 = 600;
pub(crate) fn parse_interval_secs(raw: Option<&str>) -> u64 {
raw.and_then(|v| v.trim().parse::<u64>().ok())
.filter(|n| *n > 0)
.unwrap_or(DEFAULT_INTERVAL_SECS)
}
pub(crate) fn spawn_if_enabled(
state: Arc<DaemonState>,
cancel: tokio_util::sync::CancellationToken,
) {
let raw = std::env::var(ENV_ENABLED).ok();
if !super::merged_pr_reclaim::parse_enabled(raw.as_deref()) {
info!("slot-pool eviction disabled via {ENV_ENABLED}");
return;
}
let secs = parse_interval_secs(std::env::var(ENV_INTERVAL_SECS).ok().as_deref());
tokio::spawn(evict_loop(state, Duration::from_secs(secs), cancel));
}
pub(crate) async fn evict_loop(
state: Arc<DaemonState>,
interval: Duration,
cancel: tokio_util::sync::CancellationToken,
) {
info!(
interval_secs = interval.as_secs(),
"slot-pool eviction enabled (#8451)"
);
let mut tick = tokio::time::interval(interval);
tick.set_missed_tick_behavior(MissedTickBehavior::Delay);
loop {
tokio::select! {
_ = cancel.cancelled() => break,
_ = tick.tick() => run_one_tick(&state, interval, &cancel).await,
}
}
}
pub(crate) async fn run_one_tick(
state: &Arc<DaemonState>,
interval: Duration,
cancel: &tokio_util::sync::CancellationToken,
) {
if let Some(reason) = crate::daemon::host_state_refusal(state) {
warn!("slot-pool eviction: skipped — {reason}");
return;
}
let Some(home) = dirs::home_dir() else {
warn!("slot-pool eviction: skipped — no home directory to resolve the pool from");
return;
};
let Ok(permit) = super::sweep_status::lane().acquire().await else {
warn!("slot-pool eviction: maintenance lane closed; pass not run");
return;
};
let cancel = cancel.clone();
let joined = tokio::task::spawn_blocking(move || {
let _permit = permit;
let started = Instant::now();
log_tick(&evict_in(&home, &|| cancel.is_cancelled()));
if let Some(msg) = overrun_warning(started.elapsed(), interval) {
warn!("slot-pool eviction: {msg}");
}
})
.await;
if let Err(e) = joined {
warn!("slot-pool eviction: the pass did not complete: {e}");
}
}
pub(crate) fn overrun_warning(elapsed: Duration, interval: Duration) -> Option<String> {
(elapsed > interval).then(|| {
format!(
"the pass took {}s, longer than the {}s interval; it held the maintenance \
lane throughout",
elapsed.as_secs(),
interval.as_secs()
)
})
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum TickLog {
Quiet,
Info(String),
Warn(String),
}
fn log_tick(line: &TickLog) {
match line {
TickLog::Quiet => {}
TickLog::Info(msg) => info!("slot-pool eviction: {msg}"),
TickLog::Warn(msg) => warn!("slot-pool eviction: {msg}"),
}
}
pub(crate) fn evict_in(home: &Path, cancelled: &dyn Fn() -> bool) -> TickLog {
let builders = crate::core::config::MpmConfig::load_default().builders;
let lease = BuildLeaseConfig::load_default();
let pool = builders.effective_slot_pool_root(home);
let guard = disk_usage_guard::active_threshold_at(home).threshold_pct;
let threshold = effective_evict_pct(lease.slot_pool_evict_pct, guard);
let store = SlotDir::resolve(Some(home));
let mut measure = |p: &Path| disk_usage_guard::measure(p).map(|m| m.usage_pct);
evict_measured(
&pool,
home,
store.as_ref().map_err(ToString::to_string),
threshold,
&mut measure,
cancelled,
)
}
pub(crate) fn evict_measured(
pool: &Path,
home: &Path,
store: Result<&SlotDir, String>,
threshold: u8,
measure: &mut dyn FnMut(&Path) -> Option<f32>,
cancelled: &dyn Fn() -> bool,
) -> TickLog {
let store = match store {
Ok(store) => store,
Err(e) => return TickLog::Warn(format!("no usable lease store ({e}); nothing evicted")),
};
match sweep(pool, home, store, threshold, measure, cancelled) {
SweepOutcome::NoPool | SweepOutcome::UnderThreshold(_) => TickLog::Quiet,
SweepOutcome::Refused(why) => {
TickLog::Warn(format!("refusing to sweep: {why}; nothing evicted"))
}
SweepOutcome::Unmeasurable => TickLog::Warn(format!(
"the volume holding {} cannot be measured; nothing evicted",
pool.display()
)),
SweepOutcome::Unlistable(e) => TickLog::Warn(format!(
"{} cannot be listed ({e}); nothing evicted",
pool.display()
)),
SweepOutcome::Swept(report) => {
let after = report.usage_after.map_or_else(
|| "unmeasured".to_string(),
|u| disk_usage_guard::fmt_pct(&u),
);
let summary = format!(
"threshold {threshold}%: evicted {} slot dir(s), spared {} in use, \
finished {} earlier removal(s); volume now {after}",
report.evicted.len(),
report.spared.len(),
report.leftovers_removed,
);
if report.failed() {
let failed: Vec<String> = report
.failed
.iter()
.map(|(p, e)| format!("{} ({e})", p.display()))
.collect();
TickLog::Warn(format!(
"{summary}; {} removal(s) did not complete: {}",
failed.len(),
failed.join("; ")
))
} else if report.cancelled {
TickLog::Info(format!(
"{summary}; stopped early: the daemon is shutting down"
))
} else if report.evicted.is_empty() && report.leftovers_removed == 0 {
TickLog::Warn(format!(
"{summary}; no idle slot was left to evict, so the volume stays over \
the threshold"
))
} else {
TickLog::Info(summary)
}
}
}
}
#[cfg(test)]
#[path = "slot_pool_evict_tests.rs"]
mod slot_pool_evict_tests;