use std::path::{Path, PathBuf};
use std::sync::Arc;
use parking_lot::RwLock;
use crate::engine::cache_manager::CacheManager;
use crate::engine::data_store::GraphDataStore;
use crate::engine::resource_budget::{MemoryAccounting, MemoryCategory, MemoryReservation};
use linkrs_core::StorageResult;
#[derive(Debug)]
pub struct SpillFile {
pub path: PathBuf,
pub category: MemoryCategory,
pub spilled_bytes: u64,
}
pub struct Spiller {
spill_dir: PathBuf,
accounting: Arc<MemoryAccounting>,
data_store: Arc<GraphDataStore>,
cache_manager: Arc<CacheManager>,
active_spills: RwLock<Vec<SpillFile>>,
spill_threshold_ratio: f64,
}
impl Spiller {
pub fn new(
spill_dir: PathBuf,
accounting: Arc<MemoryAccounting>,
data_store: Arc<GraphDataStore>,
cache_manager: Arc<CacheManager>,
spill_threshold_ratio: f64,
) -> Self {
Self {
spill_dir,
accounting,
data_store,
cache_manager,
active_spills: RwLock::new(Vec::new()),
spill_threshold_ratio,
}
}
pub fn try_reserve_with_spill(
&self,
category: MemoryCategory,
bytes: u64,
) -> StorageResult<MemoryReservation> {
let spiller = self.clone();
self.accounting
.try_reserve_with_spill(category, bytes, move || spiller.spill_cold_data(bytes))
}
pub fn spill_cold_data(&self, requested_bytes: u64) -> Option<u64> {
if requested_bytes == 0 {
return None;
}
if let Err(e) = std::fs::create_dir_all(&self.spill_dir) {
log::warn!(
"Failed to create spill directory {}: {}",
self.spill_dir.display(),
e
);
return None;
}
let mut total_freed: u64 = 0;
let snapshot = self.accounting.snapshot();
let cache_bytes = snapshot.categories[MemoryCategory::Cache as usize].current_bytes;
if cache_bytes > 0 {
self.cache_manager.shrink_cache();
self.cache_manager.refresh_memory_usage();
let after = self.accounting.snapshot();
let freed = cache_bytes
.saturating_sub(after.categories[MemoryCategory::Cache as usize].current_bytes);
if freed > 0 {
self.active_spills.write().push(SpillFile {
path: self.spill_dir.join("cache_eviction.spill"),
category: MemoryCategory::Cache,
spilled_bytes: freed,
});
total_freed += freed;
}
}
if total_freed > 0 {
self.accounting.release(MemoryCategory::Data, total_freed);
Some(total_freed)
} else {
None
}
}
pub fn cleanup_stale_files(&self) -> StorageResult<()> {
if !self.spill_dir.exists() {
return Ok(());
}
for entry in std::fs::read_dir(&self.spill_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("spill") {
std::fs::remove_file(&path)?;
log::debug!("Removed stale spill file: {}", path.display());
}
}
Ok(())
}
pub fn spill_dir(&self) -> &Path {
&self.spill_dir
}
pub fn active_spills(&self) -> &RwLock<Vec<SpillFile>> {
&self.active_spills
}
pub fn spill_threshold_ratio(&self) -> f64 {
self.spill_threshold_ratio
}
}
impl Clone for Spiller {
fn clone(&self) -> Self {
Self {
spill_dir: self.spill_dir.clone(),
accounting: Arc::clone(&self.accounting),
data_store: Arc::clone(&self.data_store),
cache_manager: Arc::clone(&self.cache_manager),
active_spills: RwLock::new(Vec::new()),
spill_threshold_ratio: self.spill_threshold_ratio,
}
}
}
impl Drop for Spiller {
fn drop(&mut self) {
let spills = self.active_spills.read();
for spill in spills.iter() {
let _cat = spill.category;
let _bytes = spill.spilled_bytes;
log::debug!(
"Cleaning up spill {} ({:?}, {} bytes)",
spill.path.display(),
_cat,
_bytes,
);
if let Err(e) = std::fs::remove_file(&spill.path) {
log::warn!(
"Failed to remove spill file {}: {}",
spill.path.display(),
e
);
}
}
}
}
impl std::fmt::Debug for Spiller {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Spiller")
.field("spill_dir", &self.spill_dir)
.field("spill_threshold_ratio", &self.spill_threshold_ratio)
.field("active_spills", &self.active_spills.read().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spiller_debug_info() {
let accounting = Arc::new(MemoryAccounting::new(
crate::engine::resource_budget::MemoryBudget::from_validated(1024, 256, 0.8, 0.95),
));
let data_store = Arc::new(GraphDataStore::new());
let cache_manager = Arc::new(CacheManager::new(
false,
0,
&crate::engine::config::ResourceConfig::default(),
Arc::clone(&accounting),
));
let spiller = Spiller::new(
PathBuf::from("/tmp/linkrs_test_spill"),
accounting,
data_store,
cache_manager,
0.90,
);
let debug = format!("{:?}", spiller);
assert!(debug.contains("Spiller"));
assert!(debug.contains("0.9"));
}
}