use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
use tracing::info;
use trusty_common::memory_core::PalaceRegistry;
pub const IDLE_EVICT_ENV: &str = "TRUSTY_MEMORY_IDLE_EVICT_SECS";
pub const DEFAULT_IDLE_EVICT_SECS: u64 = 300;
pub fn idle_evict_secs_from_env() -> u64 {
match std::env::var(IDLE_EVICT_ENV) {
Ok(v) => v.trim().parse::<u64>().unwrap_or(DEFAULT_IDLE_EVICT_SECS),
Err(_) => DEFAULT_IDLE_EVICT_SECS,
}
}
pub fn spawn_idle_evict_ticker(
registry: Arc<PalaceRegistry>,
shutdown_rx: watch::Receiver<bool>,
) -> Option<tokio::task::JoinHandle<()>> {
let secs = idle_evict_secs_from_env();
if secs == 0 {
info!(
env = IDLE_EVICT_ENV,
"idle-to-disk eviction disabled ({IDLE_EVICT_ENV}=0)"
);
return None;
}
Some(spawn_idle_evict_ticker_with(registry, secs, shutdown_rx))
}
pub fn spawn_idle_evict_ticker_with(
registry: Arc<PalaceRegistry>,
threshold_secs: u64,
mut shutdown_rx: watch::Receiver<bool>,
) -> tokio::task::JoinHandle<()> {
let threshold = Duration::from_secs(threshold_secs);
let tick = Duration::from_secs(threshold_secs.clamp(1, 60));
info!(
threshold_secs,
tick_secs = tick.as_secs(),
"idle-to-disk eviction ticker started"
);
tokio::spawn(async move {
loop {
tokio::select! {
_ = tokio::time::sleep(tick) => {}
res = shutdown_rx.changed() => {
if res.is_err() || *shutdown_rx.borrow() {
info!("idle-evict ticker shutting down");
return;
}
}
}
if *shutdown_rx.borrow() {
info!("idle-evict ticker shutting down");
return;
}
let evicted = registry.evict_idle(threshold);
if evicted > 0 {
info!(
evicted,
"idle-evict ticker dropped {evicted} idle palace(s)"
);
}
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::sync::atomic::Ordering;
use trusty_common::memory_core::palace::{Palace, PalaceId};
use trusty_common::memory_core::PalaceRegistry;
#[test]
#[serial]
fn idle_evict_secs_from_env_defaults_and_parses() {
let _env = crate::commands::env_test_lock().blocking_lock();
unsafe {
std::env::remove_var(IDLE_EVICT_ENV);
}
assert_eq!(idle_evict_secs_from_env(), DEFAULT_IDLE_EVICT_SECS);
unsafe {
std::env::set_var(IDLE_EVICT_ENV, "0");
}
assert_eq!(idle_evict_secs_from_env(), 0, "0 must disable eviction");
unsafe {
std::env::set_var(IDLE_EVICT_ENV, "45");
}
assert_eq!(idle_evict_secs_from_env(), 45);
unsafe {
std::env::remove_var(IDLE_EVICT_ENV);
}
}
#[tokio::test]
#[serial]
async fn spawn_disabled_returns_none() {
let _env = crate::commands::env_test_lock().lock().await;
unsafe {
std::env::set_var(IDLE_EVICT_ENV, "0");
}
let registry = Arc::new(PalaceRegistry::new());
let (_tx, rx) = watch::channel(false);
let handle = spawn_idle_evict_ticker(registry, rx);
unsafe {
std::env::remove_var(IDLE_EVICT_ENV);
}
assert!(handle.is_none(), "disabled ticker must not spawn a task");
}
#[tokio::test]
async fn ticker_evicts_idle_palace() {
let tmp = tempfile::tempdir().expect("tempdir");
let registry = Arc::new(PalaceRegistry::new());
let id = PalaceId::new("idle-ticker");
let palace = Palace {
id: id.clone(),
name: "Idle".to_string(),
description: None,
created_at: chrono::Utc::now(),
data_dir: tmp.path().join(id.as_str()),
};
let handle = registry
.create_palace(tmp.path(), palace)
.expect("create palace");
handle.last_accessed.store(0, Ordering::Relaxed);
drop(handle);
assert_eq!(registry.len(), 1);
let (_tx, rx) = watch::channel(false);
let _join = spawn_idle_evict_ticker_with(registry.clone(), 1, rx);
let deadline = std::time::Instant::now() + Duration::from_secs(5);
while !registry.is_empty() {
if std::time::Instant::now() >= deadline {
panic!("idle-evict ticker did not drop the idle palace within 5s");
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
assert!(
registry.get(&id).is_none(),
"idle palace must be evicted by the ticker"
);
}
}