use super::cache_root;
use super::cas::ContentAddressedStore;
use super::gc::{CacheGc, GcOptions};
use crate::core::action_cache::ActionCache;
use std::fs;
use std::path::Path;
use std::time::{Duration, SystemTime};
pub const MAX_BYTES_VAR: &str = "MITTS_CACHE_MAX_BYTES";
pub const INTERVAL_VAR: &str = "MITTS_GC_INTERVAL_SECS";
pub const DEFAULT_MAX_BYTES: u64 = 100 * 1024 * 1024 * 1024;
pub const DEFAULT_INTERVAL_SECS: u64 = 3600;
const MARKER: &str = ".auto-gc";
pub fn limit_from(value: Option<&str>) -> Option<u64> {
let Some(raw) = value.map(str::trim).filter(|v| !v.is_empty()) else {
return Some(DEFAULT_MAX_BYTES);
};
if raw.eq_ignore_ascii_case("off") || raw == "0" {
return None;
}
Some(parse_size(raw).unwrap_or(DEFAULT_MAX_BYTES))
}
pub fn parse_size(text: &str) -> Option<u64> {
let text = text.trim();
let split = text
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(text.len());
let (digits, suffix) = text.split_at(split);
let number: u64 = digits.parse().ok()?;
let multiple: u64 = match suffix.trim().to_ascii_uppercase().as_str() {
"" | "B" => 1,
"K" | "KB" | "KIB" => 1 << 10,
"M" | "MB" | "MIB" => 1 << 20,
"G" | "GB" | "GIB" => 1 << 30,
"T" | "TB" | "TIB" => 1 << 40,
_ => return None,
};
number.checked_mul(multiple)
}
fn interval() -> Duration {
let seconds = std::env::var(INTERVAL_VAR)
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.unwrap_or(DEFAULT_INTERVAL_SECS);
Duration::from_secs(seconds)
}
fn due(marker: &Path, interval: Duration, now: SystemTime) -> bool {
match fs::metadata(marker).and_then(|m| m.modified()) {
Ok(last) => now.duration_since(last).unwrap_or(Duration::MAX) >= interval,
Err(_) => true,
}
}
pub fn run_if_due(cache_dir: Option<&Path>) -> Option<u64> {
let limit = limit_from(std::env::var(MAX_BYTES_VAR).ok().as_deref())?;
let root = cache_root::resolve(cache_dir);
let cas_root = root.join(cache_root::CAS_DIR);
if !cas_root.join("objects").is_dir() {
return None;
}
let marker = root.join(MARKER);
if !due(&marker, interval(), SystemTime::now()) {
return None;
}
let _ = fs::write(&marker, "");
let cas = ContentAddressedStore::new(cas_root).ok()?;
let ac = ActionCache::new(root.join(cache_root::ACTION_CACHE_DIR)).ok()?;
let report = CacheGc::new(&cas, &ac)
.prune(&GcOptions {
max_bytes: Some(limit),
max_age: None,
})
.ok()?;
if report.skipped_active_build || report.objects_pruned == 0 {
return None;
}
eprintln!(
"[mitts::gc] the cache was over its limit of {limit} bytes: pruned {} objects, freed {} \
bytes, {} bytes kept (limit: {MAX_BYTES_VAR}, `off` disables)",
report.objects_pruned, report.bytes_freed, report.bytes_retained
);
Some(report.bytes_freed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sizes_take_binary_suffixes() {
assert_eq!(parse_size("1024"), Some(1024));
assert_eq!(parse_size("1K"), Some(1024));
assert_eq!(parse_size("500M"), Some(500 << 20));
assert_eq!(parse_size("2G"), Some(2 << 30));
assert_eq!(parse_size("1T"), Some(1 << 40));
assert_eq!(parse_size("3 GiB"), Some(3 << 30));
assert_eq!(parse_size("7mb"), Some(7 << 20));
}
#[test]
fn text_that_is_not_a_size_is_rejected() {
for bad in ["", "G", "-1", "1.5G", "ten", "1X"] {
assert_eq!(parse_size(bad), None, "{bad:?}");
}
assert_eq!(parse_size("18446744073709551615K"), None, "overflow");
}
#[test]
fn the_limit_defaults_and_can_be_turned_off() {
assert_eq!(limit_from(None), Some(DEFAULT_MAX_BYTES));
assert_eq!(limit_from(Some("")), Some(DEFAULT_MAX_BYTES));
assert_eq!(limit_from(Some(" ")), Some(DEFAULT_MAX_BYTES));
assert_eq!(limit_from(Some("off")), None);
assert_eq!(limit_from(Some("OFF")), None);
assert_eq!(limit_from(Some("0")), None);
assert_eq!(limit_from(Some("10G")), Some(10 << 30));
}
#[test]
fn a_value_that_is_not_a_size_keeps_the_default_limit() {
assert_eq!(limit_from(Some("lots")), Some(DEFAULT_MAX_BYTES));
}
#[test]
fn a_check_is_due_without_a_marker_and_after_the_interval() {
let dir = tempfile::tempdir().unwrap();
let marker = dir.path().join(MARKER);
assert!(due(&marker, Duration::from_secs(3600), SystemTime::now()));
fs::write(&marker, "").unwrap();
let now = SystemTime::now();
assert!(!due(&marker, Duration::from_secs(3600), now));
assert!(due(
&marker,
Duration::from_secs(3600),
now + Duration::from_secs(3601)
));
assert!(due(&marker, Duration::ZERO, now));
}
#[test]
fn a_marker_dated_in_the_future_counts_as_old() {
let dir = tempfile::tempdir().unwrap();
let marker = dir.path().join(MARKER);
fs::write(&marker, "").unwrap();
let before_the_marker = SystemTime::now() - Duration::from_secs(86_400);
assert!(due(&marker, Duration::from_secs(3600), before_the_marker));
}
}