use std::collections::HashSet;
use std::path::{Path, PathBuf};
use super::super::model::RoutineStore;
use super::{dir_size, ReapStats};
pub(super) const MAX_REPO_CACHE_DISK_BYTES_ENV: &str = "MOADIM_MAX_REPO_CACHE_DISK_BYTES";
pub(super) fn max_repo_cache_bytes() -> u64 {
std::env::var(MAX_REPO_CACHE_DISK_BYTES_ENV)
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(0)
}
pub(super) fn mirror_last_fetch_time(path: &Path) -> u64 {
std::fs::metadata(path.join("FETCH_HEAD"))
.or_else(|_err| std::fs::metadata(path))
.and_then(|meta| meta.modified())
.ok()
.and_then(|mtime| mtime.duration_since(std::time::UNIX_EPOCH).ok())
.map_or(0, |elapsed| elapsed.as_secs())
}
pub(super) fn prune_orphaned(dir: &Path, referenced: &HashSet<String>) -> ReapStats {
let Ok(entries) = std::fs::read_dir(dir) else {
return ReapStats::default();
};
let mut stats = ReapStats::default();
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|ft| ft.is_dir()) {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
if referenced.contains(&name) {
continue;
}
let size = dir_size(&entry.path());
match std::fs::remove_dir_all(entry.path()) {
Ok(()) => {
stats.removed += 1;
stats.freed_bytes += size;
log::info!(
"cleanup: removed orphaned repo mirror {name:?} ({size} bytes) — no routine references it"
);
}
Err(err) => {
log::warn!("cleanup: failed to remove orphaned repo mirror {name:?}: {err}");
}
}
}
stats
}
pub(super) struct EvictCandidate {
pub name: String,
pub path: PathBuf,
pub size: u64,
pub last_fetch: u64,
}
pub(super) fn pick_for_eviction(
mut candidates: Vec<EvictCandidate>,
cap_bytes: u64,
total_bytes: u64,
) -> Vec<EvictCandidate> {
if cap_bytes == 0 || total_bytes <= cap_bytes {
return Vec::new();
}
candidates.sort_by_key(|candidate| candidate.last_fetch);
let mut remaining = total_bytes;
let mut chosen = Vec::new();
for candidate in candidates {
if remaining <= cap_bytes {
break;
}
remaining = remaining.saturating_sub(candidate.size);
chosen.push(candidate);
}
chosen
}
pub(super) fn enforce(
dir: &Path,
cap_bytes: u64,
last_fetch_for: &dyn Fn(&Path) -> u64,
) -> ReapStats {
if cap_bytes == 0 {
return ReapStats::default();
}
let Ok(entries) = std::fs::read_dir(dir) else {
return ReapStats::default();
};
let mut total_bytes = 0_u64;
let mut candidates = Vec::new();
for entry in entries.flatten() {
if !entry.file_type().is_ok_and(|ft| ft.is_dir()) {
continue;
}
let name = entry.file_name().to_string_lossy().into_owned();
let size = dir_size(&entry.path());
total_bytes += size;
candidates.push(EvictCandidate {
name,
path: entry.path(),
size,
last_fetch: last_fetch_for(&entry.path()),
});
}
let mut stats = ReapStats::default();
for candidate in pick_for_eviction(candidates, cap_bytes, total_bytes) {
match std::fs::remove_dir_all(&candidate.path) {
Ok(()) => {
stats.removed += 1;
stats.freed_bytes += candidate.size;
log::warn!(
"cleanup: evicted repo mirror {:?} ({} bytes) — over the {} cap",
candidate.name,
candidate.size,
MAX_REPO_CACHE_DISK_BYTES_ENV
);
}
Err(err) => {
log::warn!(
"cleanup: failed to evict repo mirror {:?}: {err}",
candidate.name
);
}
}
}
stats
}
pub(crate) fn total_bytes() -> u64 {
dir_size(&crate::paths::repo_cache_root_dir())
}
pub(super) fn sweep(store: &RoutineStore) -> ReapStats {
let dir = crate::paths::repo_cache_root_dir();
let referenced = super::snapshot::snapshot_repo_cache_names(store);
let orphan_stats = prune_orphaned(&dir, &referenced);
let cap_stats = enforce(&dir, max_repo_cache_bytes(), &mirror_last_fetch_time);
ReapStats {
removed: orphan_stats.removed + cap_stats.removed,
freed_bytes: orphan_stats.freed_bytes + cap_stats.freed_bytes,
}
}
#[cfg(test)]
#[path = "repo_cache_cap_tests.rs"]
mod repo_cache_cap_tests;