sqry_core/cache/
persist.rs1use crate::cache::{CacheKey, GraphNodeSummary};
43use anyhow::{Context, Result};
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::fs;
47use std::io::Write;
48use std::path::{Path, PathBuf};
49use std::time::{Duration, SystemTime};
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CacheManifest {
66 pub bytes_by_language: HashMap<String, u64>,
68
69 pub sqry_version: String,
71
72 pub updated_at: SystemTime,
74}
75
76impl Default for CacheManifest {
77 fn default() -> Self {
78 Self {
79 bytes_by_language: HashMap::new(),
80 sqry_version: env!("CARGO_PKG_VERSION").to_string(),
81 updated_at: SystemTime::now(),
82 }
83 }
84}
85
86pub struct PersistManager {
90 cache_root: PathBuf,
92
93 user_namespace_id: String,
95}
96
97impl PersistManager {
98 pub fn new<P: AsRef<Path>>(cache_root: P) -> Result<Self> {
115 let cache_root = cache_root.as_ref().to_path_buf();
116
117 fs::create_dir_all(&cache_root).with_context(|| {
119 format!("Failed to create cache directory: {}", cache_root.display())
120 })?;
121
122 let user_namespace_id = Self::compute_user_hash();
124
125 let manager = Self {
126 cache_root,
127 user_namespace_id,
128 };
129
130 manager.cleanup_stale_locks()?;
132
133 Ok(manager)
134 }
135
136 fn compute_user_hash() -> String {
141 use std::collections::hash_map::DefaultHasher;
142 use std::hash::{Hash, Hasher};
143
144 let username = std::env::var("USER")
145 .or_else(|_| std::env::var("USERNAME"))
146 .unwrap_or_else(|_| "default".to_string());
147
148 let mut hasher = DefaultHasher::new();
149 username.hash(&mut hasher);
150 format!("{:x}", hasher.finish())
151 }
152
153 #[must_use]
157 pub fn user_cache_dir(&self) -> PathBuf {
158 self.cache_root.join(&self.user_namespace_id)
159 }
160
161 fn entry_path(&self, key: &CacheKey) -> PathBuf {
165 let storage_key = key.storage_key();
166 self.user_cache_dir().join(format!("{storage_key}.bin"))
167 }
168
169 fn lock_path(&self, key: &CacheKey) -> PathBuf {
171 let mut path = self.entry_path(key);
172 path.set_extension("bin.lock");
173 path
174 }
175
176 pub fn write_entry(&self, key: &CacheKey, summaries: &[GraphNodeSummary]) -> Result<usize> {
193 let entry_path = self.entry_path(key);
194 let lock_path = self.lock_path(key);
195
196 if let Some(parent) = entry_path.parent() {
198 fs::create_dir_all(parent)?;
199 }
200
201 let _lock_guard = Self::acquire_lock(&lock_path)?;
203
204 let data = postcard::to_allocvec(summaries).context("Failed to serialize cache entry")?;
206
207 let tmp_cache_file_path = entry_path.with_extension("tmp");
209 {
210 let mut temp_file = fs::File::create(&tmp_cache_file_path).with_context(|| {
211 format!(
212 "Failed to create temp file: {}",
213 tmp_cache_file_path.display()
214 )
215 })?;
216
217 temp_file.write_all(&data)?;
218 temp_file.sync_all()?; } #[cfg(windows)]
224 if entry_path.exists() {
225 fs::remove_file(&entry_path).with_context(|| {
226 format!("Failed to remove existing file: {}", entry_path.display())
227 })?;
228 }
229
230 match fs::rename(&tmp_cache_file_path, &entry_path) {
232 Ok(()) => {
233 log::debug!(
234 "Wrote cache entry: {} ({} bytes)",
235 entry_path.display(),
236 data.len()
237 );
238 Ok(data.len())
239 }
240 Err(e) => {
241 let _ = fs::remove_file(&tmp_cache_file_path);
243 Err(e).with_context(|| {
244 format!(
245 "Failed to rename {} to {}",
246 tmp_cache_file_path.display(),
247 entry_path.display()
248 )
249 })
250 }
251 }
252 }
253
254 pub fn read_entry(&self, key: &CacheKey) -> Result<Option<Vec<GraphNodeSummary>>> {
265 let entry_path = self.entry_path(key);
266
267 if !entry_path.exists() {
268 return Ok(None);
269 }
270
271 let data = fs::read(&entry_path)
272 .with_context(|| format!("Failed to read cache entry: {}", entry_path.display()))?;
273
274 let summaries: Vec<GraphNodeSummary> = postcard::from_bytes(&data).with_context(|| {
275 format!(
276 "Failed to deserialize cache entry: {}",
277 entry_path.display()
278 )
279 })?;
280
281 log::debug!(
282 "Read cache entry: {} ({} symbols)",
283 entry_path.display(),
284 summaries.len()
285 );
286
287 Ok(Some(summaries))
288 }
289
290 pub fn delete_entry(&self, key: &CacheKey) -> Result<()> {
295 let entry_path = self.entry_path(key);
296 let lock_path = self.lock_path(key);
297
298 if entry_path.exists() {
300 fs::remove_file(&entry_path).with_context(|| {
301 format!("Failed to delete cache entry: {}", entry_path.display())
302 })?;
303 }
304
305 if lock_path.exists() {
307 let _ = fs::remove_file(&lock_path); }
309
310 Ok(())
311 }
312
313 fn acquire_lock(lock_path: &Path) -> Result<LockGuard> {
318 let max_retries = 50;
321 let retry_delay = Duration::from_millis(100);
322
323 for attempt in 0..max_retries {
324 match fs::OpenOptions::new()
325 .write(true)
326 .create_new(true)
327 .open(lock_path)
328 {
329 Ok(mut file) => {
330 let pid = std::process::id();
332 writeln!(file, "{pid}")?;
333 file.sync_all()?;
334
335 return Ok(LockGuard {
336 path: lock_path.to_path_buf(),
337 });
338 }
339 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
340 if Self::is_lock_stale(lock_path)? {
342 let _ = fs::remove_file(lock_path);
344 continue;
345 }
346
347 if attempt < max_retries - 1 {
349 std::thread::sleep(retry_delay);
350 } else {
351 anyhow::bail!(
352 "Failed to acquire lock after {} attempts: {}",
353 max_retries,
354 lock_path.display()
355 );
356 }
357 }
358 Err(e) => {
359 return Err(e).context("Failed to create lock file");
360 }
361 }
362 }
363
364 anyhow::bail!("Failed to acquire lock: {}", lock_path.display())
365 }
366
367 fn is_lock_stale(lock_path: &Path) -> Result<bool> {
372 let content = fs::read_to_string(lock_path)?;
374 let pid = content
375 .trim()
376 .parse::<u32>()
377 .context("Failed to parse PID from lock file")?;
378
379 if !Self::process_exists(pid) {
381 log::debug!("Process {pid} no longer exists, lock is stale");
382 return Ok(true);
383 }
384
385 let metadata = fs::metadata(lock_path)?;
387 let modified = metadata.modified()?;
388 let age = SystemTime::now()
389 .duration_since(modified)
390 .unwrap_or(Duration::ZERO);
391
392 if age > Duration::from_secs(300) {
394 log::warn!(
395 "Lock held by PID {} for {:?} - forcing cleanup: {}",
396 pid,
397 age,
398 lock_path.display()
399 );
400 return Ok(true);
401 }
402
403 Ok(false)
404 }
405
406 #[cfg(unix)]
413 fn process_exists(pid: u32) -> bool {
414 #[cfg(target_os = "linux")]
415 {
416 let proc_path = format!("/proc/{pid}");
418 std::path::Path::new(&proc_path).exists()
419 }
420
421 #[cfg(not(target_os = "linux"))]
422 {
423 use nix::sys::signal::kill;
426 use nix::unistd::Pid;
427
428 match kill(Pid::from_raw(pid as i32), None) {
429 Ok(_) => true, Err(_) => false, }
432 }
433 }
434
435 #[cfg(not(unix))]
436 fn process_exists(_pid: u32) -> bool {
437 true
440 }
441
442 fn cleanup_stale_locks(&self) -> Result<()> {
444 let user_dir = self.user_cache_dir();
445
446 if !user_dir.exists() {
447 return Ok(()); }
449
450 let walker = walkdir::WalkDir::new(&user_dir)
452 .max_depth(10)
453 .into_iter()
454 .filter_map(std::result::Result::ok)
455 .filter(|e| {
456 e.path()
457 .extension()
458 .and_then(|ext| ext.to_str())
459 .is_some_and(|ext| ext == "lock")
460 });
461
462 let mut cleaned = 0;
463 for entry in walker {
464 let path = entry.path();
465
466 if Self::is_lock_stale(path)? {
467 if let Err(e) = fs::remove_file(path) {
468 log::warn!("Failed to remove stale lock {}: {}", path.display(), e);
469 } else {
470 log::debug!("Removed stale lock: {}", path.display());
471 cleaned += 1;
472 }
473 }
474 }
475
476 if cleaned > 0 {
477 log::info!("Cleaned up {cleaned} stale lock files");
478 }
479
480 Ok(())
481 }
482
483 pub fn clear_all(&self) -> Result<()> {
488 let user_dir = self.user_cache_dir();
489
490 if user_dir.exists() {
491 fs::remove_dir_all(&user_dir).with_context(|| {
492 format!("Failed to remove cache directory: {}", user_dir.display())
493 })?;
494
495 log::info!("Cleared all cache entries in {}", user_dir.display());
496 }
497
498 Ok(())
499 }
500}
501
502struct LockGuard {
506 path: PathBuf,
507}
508
509impl Drop for LockGuard {
510 fn drop(&mut self) {
511 if let Err(e) = fs::remove_file(&self.path) {
513 log::warn!("Failed to remove lock file {}: {}", self.path.display(), e);
514 }
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use crate::cache::CacheKey;
522 use crate::graph::unified::node::NodeKind;
523 use crate::hash::Blake3Hash;
524 use std::path::{Path, PathBuf};
525 use std::sync::Arc;
526 use tempfile::TempDir;
527
528 fn make_test_key() -> CacheKey {
529 let hash = Blake3Hash::from_bytes([42; 32]);
530 CacheKey::from_raw_path(PathBuf::from("/test/file.rs"), "rust", hash)
531 }
532
533 fn make_test_summary() -> GraphNodeSummary {
534 GraphNodeSummary::new(
535 Arc::from("test_fn"),
536 NodeKind::Function,
537 Arc::from(Path::new("test.rs")),
538 1,
539 0,
540 1,
541 10,
542 )
543 }
544
545 #[test]
546 fn test_persist_manager_new() {
547 let tmp_cache_dir = TempDir::new().unwrap();
548 let manager = PersistManager::new(tmp_cache_dir.path()).unwrap();
549
550 assert!(tmp_cache_dir.path().exists());
552 assert!(!manager.user_namespace_id.is_empty());
554 }
555
556 #[test]
557 fn test_write_and_read_entry() {
558 let tmp_cache_dir = TempDir::new().unwrap();
559 let manager = PersistManager::new(tmp_cache_dir.path()).unwrap();
560
561 let key = make_test_key();
562 let summaries = vec![make_test_summary()];
563
564 let bytes_written = manager.write_entry(&key, &summaries).unwrap();
566 assert!(bytes_written > 0);
567
568 let read_summaries = manager.read_entry(&key).unwrap().unwrap();
570 assert_eq!(read_summaries.len(), 1);
571 assert_eq!(read_summaries[0].name, summaries[0].name);
572 }
573
574 #[test]
575 fn test_read_nonexistent_entry() {
576 let tmp_cache_dir = TempDir::new().unwrap();
577 let manager = PersistManager::new(tmp_cache_dir.path()).unwrap();
578
579 let key = make_test_key();
580 let result = manager.read_entry(&key).unwrap();
581
582 assert!(result.is_none());
583 }
584
585 #[test]
586 fn test_delete_entry() {
587 let tmp_cache_dir = TempDir::new().unwrap();
588 let manager = PersistManager::new(tmp_cache_dir.path()).unwrap();
589
590 let key = make_test_key();
591 let summaries = vec![make_test_summary()];
592
593 manager.write_entry(&key, &summaries).unwrap();
595 assert!(manager.read_entry(&key).unwrap().is_some());
596
597 manager.delete_entry(&key).unwrap();
599 assert!(manager.read_entry(&key).unwrap().is_none());
600 }
601
602 #[test]
603 fn test_clear_all() {
604 let tmp_cache_dir = TempDir::new().unwrap();
605 let manager = PersistManager::new(tmp_cache_dir.path()).unwrap();
606
607 let key = make_test_key();
608 let summaries = vec![make_test_summary()];
609
610 manager.write_entry(&key, &summaries).unwrap();
612 assert!(manager.read_entry(&key).unwrap().is_some());
613
614 manager.clear_all().unwrap();
616 assert!(manager.read_entry(&key).unwrap().is_none());
617 }
618}