From c874fd87b8eff12dec51bab156fef52baa4cfeb9 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 17 Nov 2025 12:58:07 +0200 Subject: [PATCH 1/3] Increase cache housekeeping interval --- README.md | 1 + docs/_index.md | 2 +- src/storcaching.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 83a30e9..8e026b8 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ It supports multiple backends: Azure Blob Storage and local filesystem, to provi It caches the files in a local directory and serves them from there. Range requests are supported, but only for start offset, end limit is not implemented yet. Files are protected by upload locking to prevent concurrent uploads to the same path. +To limit disk usage, a background housekeeping loop samples disk space every five minutes and deletes the oldest cached files (older than an hour) whenever free space drops below 12%. ## Configuration diff --git a/docs/_index.md b/docs/_index.md index b4d570e..949b6f9 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -90,7 +90,7 @@ curl https://files.kernelci.org/metrics - **JWT Authentication**: Secure token-based authentication for uploads - **Multiple Storage Backends**: Currently supports Azure Blob Storage with extensible driver architecture -- **Local Caching**: Files are cached locally with automatic cleanup when disk space is low +- **Local Caching**: Files are cached locally with automatic cleanup when disk space is low; disk space is sampled every five minutes and the least recently updated cached files older than an hour are removed until space recovers - **Range Request Support**: Partial content downloads using HTTP range requests - **File Locking**: Prevents concurrent uploads to the same file path - **Prometheus Metrics**: System monitoring and metrics collection diff --git a/src/storcaching.rs b/src/storcaching.rs index ed72c3f..5adf24e 100644 --- a/src/storcaching.rs +++ b/src/storcaching.rs @@ -125,8 +125,8 @@ pub async fn cache_loop(cache_dir: &str) { tokio::time::sleep(Duration::from_millis(100)).await; } else { debug_log!("Free disk space: {}%", free_space); - // normal mode, sleep 10 seconds - tokio::time::sleep(Duration::from_secs(10)).await; + // normal mode, sleep 5 minutes between samples + tokio::time::sleep(Duration::from_secs(300)).await; } } } From 2f1c5d2b71bc103a572430c7a6a1d7ad94805f1b Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 17 Nov 2025 13:09:09 +0200 Subject: [PATCH 2/3] Add housekeeping summary logging --- README.md | 2 +- docs/_index.md | 2 +- src/storcaching.rs | 135 ++++++++++++++++++++++++++++++++++++--------- 3 files changed, 111 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 8e026b8..bc7845e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It supports multiple backends: Azure Blob Storage and local filesystem, to provi It caches the files in a local directory and serves them from there. Range requests are supported, but only for start offset, end limit is not implemented yet. Files are protected by upload locking to prevent concurrent uploads to the same path. -To limit disk usage, a background housekeeping loop samples disk space every five minutes and deletes the oldest cached files (older than an hour) whenever free space drops below 12%. +To limit disk usage, a background housekeeping loop samples disk space every five minutes and deletes the oldest cached files (older than an hour) whenever free space drops below 12%. A housekeeping summary is logged every five minutes showing the current free space, cache size, and any clean-up actions taken. ## Configuration diff --git a/docs/_index.md b/docs/_index.md index 949b6f9..91adc3a 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -90,7 +90,7 @@ curl https://files.kernelci.org/metrics - **JWT Authentication**: Secure token-based authentication for uploads - **Multiple Storage Backends**: Currently supports Azure Blob Storage with extensible driver architecture -- **Local Caching**: Files are cached locally with automatic cleanup when disk space is low; disk space is sampled every five minutes and the least recently updated cached files older than an hour are removed until space recovers +- **Local Caching**: Files are cached locally with automatic cleanup when disk space is low; disk space is sampled every five minutes, a housekeeping summary is logged at the same cadence, and the least recently updated cached files older than an hour are removed until space recovers - **Range Request Support**: Partial content downloads using HTTP range requests - **File Locking**: Prevents concurrent uploads to the same file path - **Prometheus Metrics**: System monitoring and metrics collection diff --git a/src/storcaching.rs b/src/storcaching.rs index 5adf24e..1298017 100644 --- a/src/storcaching.rs +++ b/src/storcaching.rs @@ -1,14 +1,20 @@ use crate::debug_log; use std::fs; use std::time::SystemTime; -use tokio::time::Duration; +use tokio::time::{Duration, Instant}; struct Files { file: String, last_update: SystemTime, } -async fn read_filesinfo(cache_dir: String) -> Vec { +#[derive(Default)] +struct CleanOutcome { + deleted_entries: u64, + reclaimed_bytes: u64, +} + +async fn read_filesinfo(cache_dir: &str) -> Vec { let mut files = Vec::new(); let paths = fs::read_dir(&cache_dir); match paths { @@ -55,52 +61,111 @@ async fn freediskspace_percent(cache_dir: String) -> u64 { percent as u64 } -fn delete_cache_file(file: String) { +fn delete_cache_file(file: &str) -> CleanOutcome { // Truncate from filename .content, and add .headers, delete both files - let content_filename = file.clone(); + let content_filename = file.to_string(); let headers_filename = file.replace(".content", ".headers"); debug_log!( "Deleting files: {} {}", &content_filename, &headers_filename ); - let res = fs::remove_file(&content_filename); - match res { - Ok(_) => {} + let mut outcome = CleanOutcome::default(); + let content_size = fs::metadata(&content_filename) + .map(|m| m.len()) + .unwrap_or(0); + let header_size = fs::metadata(&headers_filename) + .map(|m| m.len()) + .unwrap_or(0); + match fs::remove_file(&content_filename) { + Ok(_) => { + outcome.deleted_entries = 1; + outcome.reclaimed_bytes += content_size; + } Err(_) => { debug_log!("Error deleting file: {}", content_filename); } } - let res = fs::remove_file(&headers_filename); - match res { - Ok(_) => {} + match fs::remove_file(&headers_filename) { + Ok(_) => { + outcome.reclaimed_bytes += header_size; + } Err(_) => { debug_log!("Error deleting file: {}", headers_filename); } } + outcome } -async fn clean_disk(cache_dir: String) { +async fn clean_disk(cache_dir: &str) -> CleanOutcome { let files = read_filesinfo(cache_dir).await; - let mut oldest_file = Files { - file: "".to_string(), - last_update: SystemTime::now(), - }; + let mut oldest_file: Option = None; for file in files { - if file.last_update < oldest_file.last_update { - oldest_file = file; + if oldest_file + .as_ref() + .map(|old| file.last_update < old.last_update) + .unwrap_or(true) + { + oldest_file = Some(file); } } - // if file is less than 60 min old, do not delete it - if oldest_file.last_update.elapsed().unwrap() > Duration::from_secs(60 * 60) { - delete_cache_file(oldest_file.file); + if let Some(file) = oldest_file { + if let Ok(age) = file.last_update.elapsed() { + if age > Duration::from_secs(60 * 60) { + return delete_cache_file(&file.file); + } else { + debug_log!( + "File is less than 60 min old, skipping: {}, sleeping 60 seconds", + file.file + ); + // sleep 60 seconds + tokio::time::sleep(Duration::from_secs(60)).await; + } + } + } + CleanOutcome::default() +} + +fn format_bytes(bytes: u64) -> String { + const KB: f64 = 1024.0; + const MB: f64 = KB * 1024.0; + const GB: f64 = MB * 1024.0; + const TB: f64 = GB * 1024.0; + + if bytes >= TB as u64 { + format!("{:.1}TB", bytes as f64 / TB) + } else if bytes >= GB as u64 { + format!("{:.1}GB", bytes as f64 / GB) + } else if bytes >= MB as u64 { + format!("{:.1}MB", bytes as f64 / MB) + } else if bytes >= KB as u64 { + format!("{:.1}KB", bytes as f64 / KB) + } else { + format!("{}B", bytes) + } +} + +async fn log_housekeeping( + cache_dir: &str, + free_space: u64, + deleted_entries: u64, + reclaimed_bytes: u64, +) { + let files_in_cache = read_filesinfo(cache_dir).await.len(); + if deleted_entries > 0 { + println!( + "[housekeeping] {}% disk space remaining, {} files in cache, deleted {} {} and recovered {} space.", + free_space, + files_in_cache, + deleted_entries, + if deleted_entries == 1 { "file" } else { "files" }, + format_bytes(reclaimed_bytes) + ); } else { - debug_log!( - "File is less than 60 min old, skipping: {}, sleeping 60 seconds", - oldest_file.file + println!( + "[housekeeping] {}% disk space remaining, {} files in cache. No action taken.", + free_space, files_in_cache ); - // sleep 60 seconds - tokio::time::sleep(Duration::from_secs(60)).await; } } @@ -108,6 +173,9 @@ async fn clean_disk(cache_dir: String) { /// This function will check the disk space every and clean with some hysteresis pub async fn cache_loop(cache_dir: &str) { let mut cleaning_on: bool = false; + let mut deleted_entries_counter: u64 = 0; + let mut reclaimed_bytes_counter: u64 = 0; + let mut next_log = Instant::now(); loop { let free_space = freediskspace_percent(cache_dir.to_string()).await; if free_space < 12 && !cleaning_on { @@ -119,8 +187,23 @@ pub async fn cache_loop(cache_dir: &str) { println!("Free disk space is OK: {}%, cleaning is off", free_space); } + if Instant::now() >= next_log { + log_housekeeping( + cache_dir, + free_space, + deleted_entries_counter, + reclaimed_bytes_counter, + ) + .await; + deleted_entries_counter = 0; + reclaimed_bytes_counter = 0; + next_log = Instant::now() + Duration::from_secs(300); + } + if cleaning_on { - clean_disk(cache_dir.to_string()).await; + let outcome = clean_disk(cache_dir).await; + deleted_entries_counter += outcome.deleted_entries; + reclaimed_bytes_counter += outcome.reclaimed_bytes; // critical mode, sleep only 100ms tokio::time::sleep(Duration::from_millis(100)).await; } else { From 28d73457345a2dacb2ed1b884fac2e57635fba74 Mon Sep 17 00:00:00 2001 From: Denys Fedoryshchenko Date: Mon, 17 Nov 2025 13:21:31 +0200 Subject: [PATCH 3/3] Revamp cache housekeeping logic --- README.md | 14 +- config-local.toml | 3 + docs/_index.md | 13 +- src/storcaching.rs | 320 +++++++++++++++++++++++++++++++++------------ 4 files changed, 266 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index bc7845e..9783f31 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ It supports multiple backends: Azure Blob Storage and local filesystem, to provi It caches the files in a local directory and serves them from there. Range requests are supported, but only for start offset, end limit is not implemented yet. Files are protected by upload locking to prevent concurrent uploads to the same path. -To limit disk usage, a background housekeeping loop samples disk space every five minutes and deletes the oldest cached files (older than an hour) whenever free space drops below 12%. A housekeeping summary is logged every five minutes showing the current free space, cache size, and any clean-up actions taken. +To limit disk usage, a background housekeeping loop samples disk space every five minutes, keeps the cache below 1,000,000 items, and deletes the oldest cached files in configurable batches (default 100,000 files) whenever the file-count or disk-space thresholds are exceeded. A housekeeping summary is logged every five minutes showing the current free space, cache size, and any clean-up actions taken. ## Configuration @@ -42,6 +42,18 @@ name = "user@email.com" prefixes = ["/allowed/path"] ``` +### Cache Housekeeping + +Cache housekeeping enforces a hard limit of 1,000,000 cached artifacts (`*.content` files) and frees disk space whenever free capacity drops below 12% (stopping once it rises above 13%). Files are always deleted in least-recently-updated order. The size of each deletion batch is configurable via the optional `[cache]` section: + +```toml +[cache] +# Number of cached files deleted per housekeeping iteration when limits are exceeded +cleanup_chunk_size = 100000 # Defaults to 100000 +``` + +Larger chunk sizes reclaim space faster when the cache is far above the limit, while smaller chunks reduce the amount of data removed per iteration. + ## Creating user tokens The server uses JWT token based authentication. The token is passed in the `Authorization` header as a Bearer token. diff --git a/config-local.toml b/config-local.toml index 8fc4d3f..269340a 100644 --- a/config-local.toml +++ b/config-local.toml @@ -3,3 +3,6 @@ jwt_secret="test-secret-for-local-storage-testing" [local] storage_path="./test-storage" + +[cache] +cleanup_chunk_size=100000 diff --git a/docs/_index.md b/docs/_index.md index 91adc3a..bc367ee 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -90,7 +90,7 @@ curl https://files.kernelci.org/metrics - **JWT Authentication**: Secure token-based authentication for uploads - **Multiple Storage Backends**: Currently supports Azure Blob Storage with extensible driver architecture -- **Local Caching**: Files are cached locally with automatic cleanup when disk space is low; disk space is sampled every five minutes, a housekeeping summary is logged at the same cadence, and the least recently updated cached files older than an hour are removed until space recovers +- **Local Caching**: Files are cached locally with automatic cleanup rules; housekeeping enforces a hard limit of 1,000,000 cached entries, deletes the oldest files in configurable batches (default 100,000 files), and frees disk space whenever available space falls below 12% - **Range Request Support**: Partial content downloads using HTTP range requests - **File Locking**: Prevents concurrent uploads to the same file path - **Prometheus Metrics**: System monitoring and metrics collection @@ -157,6 +157,17 @@ name = "admin@example.com" prefixes = [""] # Empty prefix allows access to all paths ``` +### Cache Housekeeping + +The cache directory is capped at 1,000,000 cached artifacts (`*.content` files). When the limit is exceeded—or when disk space drops below 12%—the housekeeping worker deletes the oldest entries in batches. The batch size defaults to 100,000 files and can be overridden in the configuration file: + +```toml +[cache] +cleanup_chunk_size = 100000 +``` + +Raising the value makes each cleanup iteration more aggressive; lowering it favors smaller, more frequent deletions. + ## Environment Variables - `STORAGE_DEBUG`: Enable debug logging diff --git a/src/storcaching.rs b/src/storcaching.rs index 1298017..8ddaa67 100644 --- a/src/storcaching.rs +++ b/src/storcaching.rs @@ -1,47 +1,143 @@ -use crate::debug_log; +use crate::{debug_log, get_config_content}; +use serde::Deserialize; +use std::cmp::Ordering; +use std::collections::BinaryHeap; use std::fs; -use std::time::SystemTime; +use std::time::{SystemTime, UNIX_EPOCH}; use tokio::time::{Duration, Instant}; +use toml::Table; -struct Files { +const MAX_CACHE_FILES: usize = 1_000_000; +const DEFAULT_CLEANUP_CHUNK: usize = 100_000; +const DISK_SPACE_LOW_PERCENT: u64 = 12; +const DISK_SPACE_RECOVER_PERCENT: u64 = 13; +const HOUSEKEEPING_INTERVAL_SECS: u64 = 300; + +#[derive(Debug, Clone, Copy, Deserialize)] +struct CacheConfig { + #[serde(default = "default_cleanup_chunk_size")] + cleanup_chunk_size: usize, +} + +fn default_cleanup_chunk_size() -> usize { + DEFAULT_CLEANUP_CHUNK +} + +fn get_cache_config() -> CacheConfig { + let cfg_content = get_config_content(); + let cfg: Table = toml::from_str(&cfg_content).unwrap_or_else(|_| Table::new()); + let cleanup_chunk_size = cfg + .get("cache") + .and_then(|section| section.get("cleanup_chunk_size")) + .and_then(|value| value.as_integer()) + .map(|v| v.max(1) as usize) + .unwrap_or(DEFAULT_CLEANUP_CHUNK); + CacheConfig { cleanup_chunk_size } +} + +#[derive(Clone)] +struct CacheFile { file: String, last_update: SystemTime, } +impl CacheFile { + fn modified_duration(&self) -> Duration { + self.last_update + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|_| Duration::from_secs(0)) + } +} + +impl PartialEq for CacheFile { + fn eq(&self, other: &Self) -> bool { + self.modified_duration() == other.modified_duration() + } +} + +impl Eq for CacheFile {} + +impl PartialOrd for CacheFile { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for CacheFile { + fn cmp(&self, other: &Self) -> Ordering { + self.modified_duration().cmp(&other.modified_duration()) + } +} + +#[derive(Default)] +struct CacheScanResult { + total_files: usize, + oldest_files: Vec, +} + #[derive(Default)] struct CleanOutcome { deleted_entries: u64, reclaimed_bytes: u64, } -async fn read_filesinfo(cache_dir: &str) -> Vec { - let mut files = Vec::new(); - let paths = fs::read_dir(&cache_dir); - match paths { - Ok(paths) => { - for path in paths { - let path = path.unwrap().path(); - let file = path.to_str().unwrap().to_string(); - let metadata = fs::metadata(&file).unwrap(); - let last_update = metadata.modified().unwrap(); - // is this file ending with ".content"? - if !file.ends_with(".content") { - continue; - } - files.push(Files { file, last_update }); - } - files +fn scan_cache_directory(cache_dir: &str, chunk_size: usize) -> CacheScanResult { + let mut result = CacheScanResult::default(); + let entries = match fs::read_dir(cache_dir) { + Ok(entries) => entries, + Err(e) => { + eprintln!("Error reading cache directory files ({}): {}", cache_dir, e); + return result; } - Err(_) => { - eprintln!("Error reading cache directory files"); - Vec::new() + }; + + let mut heap = if chunk_size > 0 { + Some(BinaryHeap::with_capacity(chunk_size.saturating_add(1))) + } else { + None + }; + + for entry in entries.flatten() { + let path = entry.path(); + let file = match path.to_str() { + Some(path_str) => path_str.to_string(), + None => continue, + }; + + if !file.ends_with(".content") { + continue; + } + + result.total_files += 1; + + if let Some(heap) = heap.as_mut() { + let metadata = match entry.metadata() { + Ok(metadata) => metadata, + Err(_) => continue, + }; + let last_update = match metadata.modified() { + Ok(last_update) => last_update, + Err(_) => continue, + }; + heap.push(CacheFile { file, last_update }); + if heap.len() > chunk_size { + heap.pop(); + } } } + + if let Some(heap) = heap { + let mut oldest_files = heap.into_sorted_vec(); + oldest_files.truncate(chunk_size); + result.oldest_files = oldest_files; + } + + result } -async fn freediskspace_percent(cache_dir: String) -> u64 { - let total_r = fs2::total_space(&cache_dir); - let free_r = fs2::available_space(&cache_dir); +async fn freediskspace_percent(cache_dir: &str) -> u64 { + let total_r = fs2::total_space(cache_dir); + let free_r = fs2::available_space(cache_dir); let total = match total_r { Ok(total) => total as f64, Err(_) => { @@ -97,35 +193,6 @@ fn delete_cache_file(file: &str) -> CleanOutcome { outcome } -async fn clean_disk(cache_dir: &str) -> CleanOutcome { - let files = read_filesinfo(cache_dir).await; - let mut oldest_file: Option = None; - for file in files { - if oldest_file - .as_ref() - .map(|old| file.last_update < old.last_update) - .unwrap_or(true) - { - oldest_file = Some(file); - } - } - if let Some(file) = oldest_file { - if let Ok(age) = file.last_update.elapsed() { - if age > Duration::from_secs(60 * 60) { - return delete_cache_file(&file.file); - } else { - debug_log!( - "File is less than 60 min old, skipping: {}, sleeping 60 seconds", - file.file - ); - // sleep 60 seconds - tokio::time::sleep(Duration::from_secs(60)).await; - } - } - } - CleanOutcome::default() -} - fn format_bytes(bytes: u64) -> String { const KB: f64 = 1024.0; const MB: f64 = KB * 1024.0; @@ -145,13 +212,95 @@ fn format_bytes(bytes: u64) -> String { } } -async fn log_housekeeping( +async fn enforce_cache_file_limit(cache_dir: &str, chunk_size: usize) -> (CleanOutcome, usize) { + let mut outcome = CleanOutcome::default(); + + loop { + let scan = scan_cache_directory(cache_dir, chunk_size); + if scan.total_files <= MAX_CACHE_FILES { + return (outcome, scan.total_files); + } + + if scan.oldest_files.is_empty() { + debug_log!( + "Cache file limit exceeded ({} items) but no deletable files were found", + scan.total_files + ); + return (outcome, scan.total_files); + } + + let mut deleted_any = false; + for entry in &scan.oldest_files { + let res = delete_cache_file(&entry.file); + if res.deleted_entries > 0 { + deleted_any = true; + } + outcome.deleted_entries += res.deleted_entries; + outcome.reclaimed_bytes += res.reclaimed_bytes; + } + + if !deleted_any { + debug_log!("Cache file limit cleanup could not delete any files, stopping iteration"); + return (outcome, scan.total_files); + } + } +} + +struct DiskCleanupResult { + outcome: CleanOutcome, + final_free_space: u64, +} + +async fn enforce_disk_space( cache_dir: &str, + chunk_size: usize, + mut free_space: u64, +) -> DiskCleanupResult { + let mut outcome = CleanOutcome::default(); + + while free_space < DISK_SPACE_LOW_PERCENT { + let scan = scan_cache_directory(cache_dir, chunk_size); + if scan.oldest_files.is_empty() { + debug_log!( + "Disk space is low ({}%), but no cache files are available for removal", + free_space + ); + break; + } + + let mut deleted_any = false; + for entry in &scan.oldest_files { + let res = delete_cache_file(&entry.file); + if res.deleted_entries > 0 { + deleted_any = true; + } + outcome.deleted_entries += res.deleted_entries; + outcome.reclaimed_bytes += res.reclaimed_bytes; + } + + if !deleted_any { + debug_log!("Failed to delete files during disk cleanup, aborting"); + break; + } + + free_space = freediskspace_percent(cache_dir).await; + if free_space >= DISK_SPACE_RECOVER_PERCENT { + break; + } + } + + DiskCleanupResult { + outcome, + final_free_space: free_space, + } +} + +fn log_housekeeping( free_space: u64, + files_in_cache: usize, deleted_entries: u64, reclaimed_bytes: u64, ) { - let files_in_cache = read_filesinfo(cache_dir).await.len(); if deleted_entries > 0 { println!( "[housekeeping] {}% disk space remaining, {} files in cache, deleted {} {} and recovered {} space.", @@ -170,46 +319,53 @@ async fn log_housekeeping( } /// Cache housekeeping loop -/// This function will check the disk space every and clean with some hysteresis +/// Enforces cache size limits and disk space thresholds with periodic logging pub async fn cache_loop(cache_dir: &str) { - let mut cleaning_on: bool = false; + let config = get_cache_config(); + let cleanup_chunk_size = config.cleanup_chunk_size.max(1); let mut deleted_entries_counter: u64 = 0; let mut reclaimed_bytes_counter: u64 = 0; + let mut cached_file_count: usize = 0; let mut next_log = Instant::now(); + loop { - let free_space = freediskspace_percent(cache_dir.to_string()).await; - if free_space < 12 && !cleaning_on { - cleaning_on = true; - println!("Free disk is LOW: {}%, cleaning is on", free_space); - } - if free_space > 13 && cleaning_on { - cleaning_on = false; - println!("Free disk space is OK: {}%, cleaning is off", free_space); + let (limit_outcome, file_count) = + enforce_cache_file_limit(cache_dir, cleanup_chunk_size).await; + deleted_entries_counter += limit_outcome.deleted_entries; + reclaimed_bytes_counter += limit_outcome.reclaimed_bytes; + cached_file_count = file_count; + + let mut free_space = freediskspace_percent(cache_dir).await; + if free_space < DISK_SPACE_LOW_PERCENT { + println!( + "Free disk is LOW: {}%, starting cache eviction.", + free_space + ); + let disk_result = enforce_disk_space(cache_dir, cleanup_chunk_size, free_space).await; + deleted_entries_counter += disk_result.outcome.deleted_entries; + reclaimed_bytes_counter += disk_result.outcome.reclaimed_bytes; + cached_file_count = + cached_file_count.saturating_sub(disk_result.outcome.deleted_entries as usize); + free_space = disk_result.final_free_space; + if free_space >= DISK_SPACE_RECOVER_PERCENT { + println!("Free disk space is OK: {}%, stopping eviction.", free_space); + } + } else { + debug_log!("Free disk space: {}%", free_space); } if Instant::now() >= next_log { log_housekeeping( - cache_dir, free_space, + cached_file_count, deleted_entries_counter, reclaimed_bytes_counter, - ) - .await; + ); deleted_entries_counter = 0; reclaimed_bytes_counter = 0; - next_log = Instant::now() + Duration::from_secs(300); + next_log = Instant::now() + Duration::from_secs(HOUSEKEEPING_INTERVAL_SECS); } - if cleaning_on { - let outcome = clean_disk(cache_dir).await; - deleted_entries_counter += outcome.deleted_entries; - reclaimed_bytes_counter += outcome.reclaimed_bytes; - // critical mode, sleep only 100ms - tokio::time::sleep(Duration::from_millis(100)).await; - } else { - debug_log!("Free disk space: {}%", free_space); - // normal mode, sleep 5 minutes between samples - tokio::time::sleep(Duration::from_secs(300)).await; - } + tokio::time::sleep(Duration::from_secs(HOUSEKEEPING_INTERVAL_SECS)).await; } }