merge: hot.rs

This commit is contained in:
Omar Sobh
2026-06-16 03:53:19 +00:00
+123
View File
@@ -0,0 +1,123 @@
use crate::manifest::{Manifest, Project};
use anyhow::{Context, Result};
use chrono::Utc;
use std::path::Path;
pub fn target_size_bytes(path: &Path) -> Result<u64> {
if !path.exists() { return Ok(0); }
let out = std::process::Command::new("du")
.args(["-sb", path.to_str().unwrap()])
.output()
.context("running du")?;
let line = String::from_utf8_lossy(&out.stdout);
let bytes = line.split_whitespace()
.next()
.and_then(|s| s.parse::<u64>().ok())
.unwrap_or(0);
Ok(bytes)
}
pub fn total_used_gb(manifest: &Manifest) -> Result<f64> {
let mut total = 0u64;
for p in &manifest.projects {
total += target_size_bytes(&p.hot_target_path)?;
}
Ok(total as f64 / 1_073_741_824.0)
}
pub fn gc_stale_targets(manifest: &Manifest, stale_hours: u64) -> Result<Vec<String>> {
let now = Utc::now();
let threshold = chrono::Duration::hours(stale_hours as i64);
let mut evicted = Vec::new();
for p in &manifest.projects {
let is_stale = match p.last_active {
None => true,
Some(t) => (now - t) > threshold,
};
if is_stale && p.hot_target_path.exists() {
std::fs::remove_dir_all(&p.hot_target_path)
.with_context(|| format!("removing hot target for {}", p.name))?;
evicted.push(p.name.clone());
}
}
Ok(evicted)
}
pub fn gc_by_space(manifest: &mut Manifest, max_gb: f64) -> Result<Vec<String>> {
let mut evicted = Vec::new();
loop {
let used = total_used_gb(manifest)?;
if used <= max_gb { break; }
let lru_name = manifest.projects.iter()
.filter(|p| p.hot_target_path.exists())
.min_by_key(|p| p.last_active)
.map(|p| p.name.clone());
match lru_name {
None => break,
Some(name) => {
if let Some(p) = manifest.projects.iter().find(|p| p.name == name) {
if p.hot_target_path.exists() {
std::fs::remove_dir_all(&p.hot_target_path)?;
evicted.push(name);
}
}
}
}
}
Ok(evicted)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{Manifest, Project};
use tempfile::TempDir;
fn make_project(dir: &TempDir, name: &str) -> Project {
let path = dir.path().join(name);
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join("dummy"), vec![0u8; 1024 * 1024]).unwrap();
Project {
name: name.into(),
warm_path: format!("/slab/projects/{}", name).into(),
hot_target_path: path.clone(),
last_build: None,
last_active: None,
}
}
#[test]
fn test_project_target_size_bytes() {
let dir = TempDir::new().unwrap();
let p = make_project(&dir, "test-proj");
let size = target_size_bytes(&p.hot_target_path).unwrap();
assert!(size >= 1024 * 1024, "expected at least 1MB, got {}", size);
}
#[test]
fn test_gc_evicts_stale_project() {
let dir = TempDir::new().unwrap();
let mut manifest = Manifest::default();
manifest.projects.push(make_project(&dir, "stale-proj"));
manifest.projects[0].last_active =
Some(chrono::Utc::now() - chrono::Duration::hours(100));
let evicted = gc_stale_targets(&manifest, 48).unwrap();
assert_eq!(evicted.len(), 1);
assert_eq!(evicted[0], "stale-proj");
assert!(!dir.path().join("stale-proj").exists());
}
#[test]
fn test_gc_skips_active_project() {
let dir = TempDir::new().unwrap();
let mut manifest = Manifest::default();
manifest.projects.push(make_project(&dir, "active-proj"));
manifest.projects[0].last_active =
Some(chrono::Utc::now() - chrono::Duration::hours(2));
let evicted = gc_stale_targets(&manifest, 48).unwrap();
assert!(evicted.is_empty());
assert!(dir.path().join("active-proj").exists());
}
}