use crate::fscache::CacheSystem;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::Notify;
#[derive(Debug)]
pub struct OverflowRecovery {
overflow: Notify,
build_trigger: Notify,
pending: AtomicBool,
delay: Duration,
}
impl OverflowRecovery {
#[must_use]
pub fn new(delay: Duration) -> Self {
Self {
overflow: Notify::new(),
build_trigger: Notify::new(),
pending: AtomicBool::new(false),
delay,
}
}
#[must_use]
pub fn default_delay() -> Self {
Self::new(Duration::from_secs(30))
}
#[must_use]
pub fn is_pending(&self) -> bool {
self.pending.load(Ordering::Acquire)
}
pub fn on_overflow(&self) {
tracing::info!("overflow recovery: overflow detected, scheduling deferred rescan");
self.pending.store(true, Ordering::Release);
self.overflow.notify_one();
}
pub fn on_build_event(&self) {
if self.pending.load(Ordering::Acquire) {
tracing::info!(
"overflow recovery: build event received, \
cancelling deferred rescan and triggering immediately"
);
self.build_trigger.notify_one();
}
}
pub async fn run(&self, cache: &CacheSystem) {
loop {
self.overflow.notified().await;
tracing::info!(
delay_secs = self.delay.as_secs(),
"overflow recovery: rescan scheduled"
);
tokio::select! {
() = tokio::time::sleep(self.delay) => {
tracing::info!(
"overflow recovery: delay elapsed, starting deferred rescan"
);
}
() = self.build_trigger.notified() => {
tracing::info!(
"overflow recovery: build event triggered immediate rescan, \
deferred timer cancelled"
);
}
}
let promoted = cache.rescan_entries();
self.pending.store(false, Ordering::Release);
tracing::info!(
promoted,
"overflow recovery: rescan complete, deferred task removed"
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::NormalizedPath;
use crate::fscache::clock::Clock;
use crate::fscache::Confidence;
use std::fs;
use std::sync::Arc;
use tempfile::TempDir;
fn create_file(dir: &TempDir, name: &str, content: &str) -> NormalizedPath {
let path = dir.path().join(name);
fs::write(&path, content).expect("failed to create test file");
path.into()
}
#[tokio::test]
async fn recovery_rescans_after_delay() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.apply_overflow();
assert_eq!(
cache.metadata().get(&path).unwrap().confidence,
Confidence::Low
);
let recovery = Arc::new(OverflowRecovery::new(Duration::from_millis(50)));
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_overflow();
tokio::time::sleep(Duration::from_millis(150)).await;
assert_eq!(
cache.metadata().get(&path).unwrap().confidence,
Confidence::High
);
handle.abort();
}
#[tokio::test]
async fn recovery_rescans_immediately_on_build_event() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.apply_overflow();
let recovery = Arc::new(OverflowRecovery::new(Duration::from_secs(60)));
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_overflow();
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
loop {
tokio::time::sleep(Duration::from_millis(5)).await;
recovery.on_build_event();
if cache.metadata().get(&path).unwrap().confidence == Confidence::High {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"timed out waiting for build-triggered rescan"
);
}
handle.abort();
}
#[tokio::test]
async fn no_overflow_means_no_rescan() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.metadata().downgrade_all();
let recovery = Arc::new(OverflowRecovery::new(Duration::from_millis(10)));
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_build_event();
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
cache.metadata().get(&path).unwrap().confidence,
Confidence::Low
);
handle.abort();
}
#[tokio::test]
async fn default_delay_creates_recovery() {
let recovery = OverflowRecovery::default_delay();
assert_eq!(recovery.delay, Duration::from_secs(30));
}
#[tokio::test]
async fn pending_flag_tracks_state() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.apply_overflow();
let recovery = Arc::new(OverflowRecovery::new(Duration::from_millis(50)));
assert!(!recovery.is_pending());
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_overflow();
assert!(recovery.is_pending());
tokio::time::sleep(Duration::from_millis(150)).await;
assert!(!recovery.is_pending());
handle.abort();
}
#[tokio::test]
async fn build_event_is_noop_without_pending_overflow() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.metadata().downgrade_all();
let recovery = Arc::new(OverflowRecovery::new(Duration::from_millis(10)));
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_build_event();
recovery.on_build_event();
recovery.on_build_event();
assert!(!recovery.is_pending());
tokio::time::sleep(Duration::from_millis(50)).await;
assert_eq!(
cache.metadata().get(&path).unwrap().confidence,
Confidence::Low
);
handle.abort();
}
#[tokio::test]
async fn multiple_overflows_coalesce() {
let dir = TempDir::new().unwrap();
let path = create_file(&dir, "a.h", "content");
let cache = Arc::new(CacheSystem::new());
cache.lookup_since(&path, Clock::ZERO).unwrap();
cache.apply_overflow();
let recovery = Arc::new(OverflowRecovery::new(Duration::from_millis(30)));
let r = Arc::clone(&recovery);
let c = Arc::clone(&cache);
let handle = tokio::spawn(async move { r.run(&c).await });
recovery.on_overflow();
recovery.on_overflow();
recovery.on_overflow();
let deadline = tokio::time::Instant::now() + Duration::from_secs(2);
loop {
tokio::time::sleep(Duration::from_millis(5)).await;
if cache.metadata().get(&path).unwrap().confidence == Confidence::High {
break;
}
assert!(
tokio::time::Instant::now() < deadline,
"timed out waiting for overflow recovery rescan"
);
}
handle.abort();
}
}