Skip to main content

sqry_core/session/
cache.rs

1//! Cached index metadata and access tracking for session management.
2//!
3//! `CachedIndex` wraps a `CodeGraph` (unified graph format) with lightweight
4//! bookkeeping so the session manager can implement LRU and idle eviction
5//! without cloning large data structures.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9use std::time::{Instant, SystemTime};
10
11use crate::graph::CodeGraph;
12
13/// Wrapper around a `CodeGraph` with access metadata for eviction policies.
14pub struct CachedIndex {
15    /// Shared handle to the unified code graph.
16    pub graph: Arc<CodeGraph>,
17    /// Time the graph was first loaded into cache.
18    pub loaded_at: Instant,
19    /// Last time the graph was accessed.
20    last_accessed: AtomicInstant,
21    /// Modification time of the on-disk graph file when loaded.
22    pub file_mtime: SystemTime,
23    /// Number of queries served from this cache entry.
24    pub query_count: AtomicU64,
25}
26
27impl CachedIndex {
28    /// Create a new cached index wrapper with timestamps initialised to now.
29    #[must_use]
30    pub fn new(graph: Arc<CodeGraph>, file_mtime: SystemTime) -> Self {
31        let now = Instant::now();
32        Self {
33            graph,
34            loaded_at: now,
35            last_accessed: AtomicInstant::new(now),
36            file_mtime,
37            query_count: AtomicU64::new(0),
38        }
39    }
40
41    /// Record a cache access, updating timestamps and counters.
42    pub fn access(&self) {
43        self.last_accessed.store(Instant::now());
44        self.query_count.fetch_add(1, Ordering::Relaxed);
45    }
46
47    /// Retrieve the last access time.
48    pub fn last_accessed(&self) -> Instant {
49        self.last_accessed.load()
50    }
51
52    /// Force-set the last access time (used in tests to simulate staleness).
53    #[cfg(test)]
54    pub fn set_last_accessed(&self, instant: Instant) {
55        self.last_accessed.store(instant);
56    }
57
58    /// Number of queries served by this cached entry.
59    pub fn query_count(&self) -> u64 {
60        self.query_count.load(Ordering::Relaxed)
61    }
62}
63
64/// Minimal atomic wrapper for `Instant` using a mutex (Instant lacks atomics).
65pub struct AtomicInstant {
66    inner: Mutex<Instant>,
67}
68
69impl AtomicInstant {
70    fn new(instant: Instant) -> Self {
71        Self {
72            inner: Mutex::new(instant),
73        }
74    }
75
76    fn store(&self, instant: Instant) {
77        // Recover from poisoned lock - timestamp data remains valid
78        *self
79            .inner
80            .lock()
81            .unwrap_or_else(std::sync::PoisonError::into_inner) = instant;
82    }
83
84    fn load(&self) -> Instant {
85        // Recover from poisoned lock - timestamp data remains valid
86        *self
87            .inner
88            .lock()
89            .unwrap_or_else(std::sync::PoisonError::into_inner)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use std::time::Duration;
97
98    #[test]
99    fn access_updates_tracking() {
100        let graph = Arc::new(CodeGraph::new());
101        let cached = CachedIndex::new(graph, SystemTime::now());
102
103        assert_eq!(cached.query_count(), 0);
104        let before = cached.last_accessed();
105
106        cached.access();
107
108        assert_eq!(cached.query_count(), 1);
109        assert!(cached.last_accessed() >= before);
110    }
111
112    #[test]
113    fn set_last_accessed_allows_manual_adjustment() {
114        let graph = Arc::new(CodeGraph::new());
115        let cached = CachedIndex::new(graph, SystemTime::now());
116        let past = Instant::now().checked_sub(Duration::from_secs(5)).unwrap();
117
118        cached.set_last_accessed(past);
119
120        assert!(cached.last_accessed() <= Instant::now());
121    }
122}