Skip to content

Commit 2f1c5d2

Browse files
committed
Add housekeeping summary logging
1 parent c874fd8 commit 2f1c5d2

3 files changed

Lines changed: 111 additions & 28 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ It supports multiple backends: Azure Blob Storage and local filesystem, to provi
55
It caches the files in a local directory and serves them from there.
66
Range requests are supported, but only for start offset, end limit is not implemented yet.
77
Files are protected by upload locking to prevent concurrent uploads to the same path.
8-
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%.
8+
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.
99

1010
## Configuration
1111

docs/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ curl https://files.kernelci.org/metrics
9090

9191
- **JWT Authentication**: Secure token-based authentication for uploads
9292
- **Multiple Storage Backends**: Currently supports Azure Blob Storage with extensible driver architecture
93-
- **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
93+
- **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
9494
- **Range Request Support**: Partial content downloads using HTTP range requests
9595
- **File Locking**: Prevents concurrent uploads to the same file path
9696
- **Prometheus Metrics**: System monitoring and metrics collection

src/storcaching.rs

Lines changed: 109 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
use crate::debug_log;
22
use std::fs;
33
use std::time::SystemTime;
4-
use tokio::time::Duration;
4+
use tokio::time::{Duration, Instant};
55

66
struct Files {
77
file: String,
88
last_update: SystemTime,
99
}
1010

11-
async fn read_filesinfo(cache_dir: String) -> Vec<Files> {
11+
#[derive(Default)]
12+
struct CleanOutcome {
13+
deleted_entries: u64,
14+
reclaimed_bytes: u64,
15+
}
16+
17+
async fn read_filesinfo(cache_dir: &str) -> Vec<Files> {
1218
let mut files = Vec::new();
1319
let paths = fs::read_dir(&cache_dir);
1420
match paths {
@@ -55,59 +61,121 @@ async fn freediskspace_percent(cache_dir: String) -> u64 {
5561
percent as u64
5662
}
5763

58-
fn delete_cache_file(file: String) {
64+
fn delete_cache_file(file: &str) -> CleanOutcome {
5965
// Truncate from filename .content, and add .headers, delete both files
60-
let content_filename = file.clone();
66+
let content_filename = file.to_string();
6167
let headers_filename = file.replace(".content", ".headers");
6268
debug_log!(
6369
"Deleting files: {} {}",
6470
&content_filename,
6571
&headers_filename
6672
);
67-
let res = fs::remove_file(&content_filename);
68-
match res {
69-
Ok(_) => {}
73+
let mut outcome = CleanOutcome::default();
74+
let content_size = fs::metadata(&content_filename)
75+
.map(|m| m.len())
76+
.unwrap_or(0);
77+
let header_size = fs::metadata(&headers_filename)
78+
.map(|m| m.len())
79+
.unwrap_or(0);
80+
match fs::remove_file(&content_filename) {
81+
Ok(_) => {
82+
outcome.deleted_entries = 1;
83+
outcome.reclaimed_bytes += content_size;
84+
}
7085
Err(_) => {
7186
debug_log!("Error deleting file: {}", content_filename);
7287
}
7388
}
74-
let res = fs::remove_file(&headers_filename);
75-
match res {
76-
Ok(_) => {}
89+
match fs::remove_file(&headers_filename) {
90+
Ok(_) => {
91+
outcome.reclaimed_bytes += header_size;
92+
}
7793
Err(_) => {
7894
debug_log!("Error deleting file: {}", headers_filename);
7995
}
8096
}
97+
outcome
8198
}
8299

83-
async fn clean_disk(cache_dir: String) {
100+
async fn clean_disk(cache_dir: &str) -> CleanOutcome {
84101
let files = read_filesinfo(cache_dir).await;
85-
let mut oldest_file = Files {
86-
file: "".to_string(),
87-
last_update: SystemTime::now(),
88-
};
102+
let mut oldest_file: Option<Files> = None;
89103
for file in files {
90-
if file.last_update < oldest_file.last_update {
91-
oldest_file = file;
104+
if oldest_file
105+
.as_ref()
106+
.map(|old| file.last_update < old.last_update)
107+
.unwrap_or(true)
108+
{
109+
oldest_file = Some(file);
92110
}
93111
}
94-
// if file is less than 60 min old, do not delete it
95-
if oldest_file.last_update.elapsed().unwrap() > Duration::from_secs(60 * 60) {
96-
delete_cache_file(oldest_file.file);
112+
if let Some(file) = oldest_file {
113+
if let Ok(age) = file.last_update.elapsed() {
114+
if age > Duration::from_secs(60 * 60) {
115+
return delete_cache_file(&file.file);
116+
} else {
117+
debug_log!(
118+
"File is less than 60 min old, skipping: {}, sleeping 60 seconds",
119+
file.file
120+
);
121+
// sleep 60 seconds
122+
tokio::time::sleep(Duration::from_secs(60)).await;
123+
}
124+
}
125+
}
126+
CleanOutcome::default()
127+
}
128+
129+
fn format_bytes(bytes: u64) -> String {
130+
const KB: f64 = 1024.0;
131+
const MB: f64 = KB * 1024.0;
132+
const GB: f64 = MB * 1024.0;
133+
const TB: f64 = GB * 1024.0;
134+
135+
if bytes >= TB as u64 {
136+
format!("{:.1}TB", bytes as f64 / TB)
137+
} else if bytes >= GB as u64 {
138+
format!("{:.1}GB", bytes as f64 / GB)
139+
} else if bytes >= MB as u64 {
140+
format!("{:.1}MB", bytes as f64 / MB)
141+
} else if bytes >= KB as u64 {
142+
format!("{:.1}KB", bytes as f64 / KB)
143+
} else {
144+
format!("{}B", bytes)
145+
}
146+
}
147+
148+
async fn log_housekeeping(
149+
cache_dir: &str,
150+
free_space: u64,
151+
deleted_entries: u64,
152+
reclaimed_bytes: u64,
153+
) {
154+
let files_in_cache = read_filesinfo(cache_dir).await.len();
155+
if deleted_entries > 0 {
156+
println!(
157+
"[housekeeping] {}% disk space remaining, {} files in cache, deleted {} {} and recovered {} space.",
158+
free_space,
159+
files_in_cache,
160+
deleted_entries,
161+
if deleted_entries == 1 { "file" } else { "files" },
162+
format_bytes(reclaimed_bytes)
163+
);
97164
} else {
98-
debug_log!(
99-
"File is less than 60 min old, skipping: {}, sleeping 60 seconds",
100-
oldest_file.file
165+
println!(
166+
"[housekeeping] {}% disk space remaining, {} files in cache. No action taken.",
167+
free_space, files_in_cache
101168
);
102-
// sleep 60 seconds
103-
tokio::time::sleep(Duration::from_secs(60)).await;
104169
}
105170
}
106171

107172
/// Cache housekeeping loop
108173
/// This function will check the disk space every and clean with some hysteresis
109174
pub async fn cache_loop(cache_dir: &str) {
110175
let mut cleaning_on: bool = false;
176+
let mut deleted_entries_counter: u64 = 0;
177+
let mut reclaimed_bytes_counter: u64 = 0;
178+
let mut next_log = Instant::now();
111179
loop {
112180
let free_space = freediskspace_percent(cache_dir.to_string()).await;
113181
if free_space < 12 && !cleaning_on {
@@ -119,8 +187,23 @@ pub async fn cache_loop(cache_dir: &str) {
119187
println!("Free disk space is OK: {}%, cleaning is off", free_space);
120188
}
121189

190+
if Instant::now() >= next_log {
191+
log_housekeeping(
192+
cache_dir,
193+
free_space,
194+
deleted_entries_counter,
195+
reclaimed_bytes_counter,
196+
)
197+
.await;
198+
deleted_entries_counter = 0;
199+
reclaimed_bytes_counter = 0;
200+
next_log = Instant::now() + Duration::from_secs(300);
201+
}
202+
122203
if cleaning_on {
123-
clean_disk(cache_dir.to_string()).await;
204+
let outcome = clean_disk(cache_dir).await;
205+
deleted_entries_counter += outcome.deleted_entries;
206+
reclaimed_bytes_counter += outcome.reclaimed_bytes;
124207
// critical mode, sleep only 100ms
125208
tokio::time::sleep(Duration::from_millis(100)).await;
126209
} else {

0 commit comments

Comments
 (0)