Skip to main content

sochdb_storage/
lazy_namespace.rs

1//! Per-namespace lazy hydrate/evict — resident memory tracks active tenants.
2
3use parking_lot::RwLock;
4use std::collections::{HashMap, VecDeque};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone)]
9pub struct LazyNamespaceConfig {
10    pub max_resident: usize,
11    pub idle_evict_after: Duration,
12}
13
14impl Default for LazyNamespaceConfig {
15    fn default() -> Self {
16        Self {
17            max_resident: 1024,
18            idle_evict_after: Duration::from_secs(300),
19        }
20    }
21}
22
23#[derive(Debug)]
24struct NamespaceHandle {
25    namespace: String,
26    hydrated: bool,
27    last_access: Instant,
28    epoch: u64,
29}
30
31/// Handle table with LRU eviction under epoch guard.
32pub struct LazyNamespaceTable {
33    config: LazyNamespaceConfig,
34    handles: RwLock<HashMap<String, NamespaceHandle>>,
35    lru: RwLock<VecDeque<String>>,
36    global_epoch: AtomicU64,
37    evictions: AtomicU64,
38}
39
40impl LazyNamespaceTable {
41    pub fn new(config: LazyNamespaceConfig) -> Self {
42        Self {
43            config,
44            handles: RwLock::new(HashMap::new()),
45            lru: RwLock::new(VecDeque::new()),
46            global_epoch: AtomicU64::new(0),
47            evictions: AtomicU64::new(0),
48        }
49    }
50
51    pub fn resolve(&self, namespace: &str) -> u64 {
52        let epoch = self.global_epoch.fetch_add(1, Ordering::SeqCst);
53        let mut handles = self.handles.write();
54        let entry = handles
55            .entry(namespace.to_string())
56            .or_insert_with(|| NamespaceHandle {
57                namespace: namespace.to_string(),
58                hydrated: false,
59                last_access: Instant::now(),
60                epoch,
61            });
62        entry.last_access = Instant::now();
63        entry.epoch = epoch;
64        if !entry.hydrated {
65            entry.hydrated = true;
66        }
67        self.touch_lru(namespace);
68        epoch
69    }
70
71    fn touch_lru(&self, namespace: &str) {
72        let mut lru = self.lru.write();
73        lru.retain(|n| n != namespace);
74        lru.push_front(namespace.to_string());
75        while lru.len() > self.config.max_resident {
76            if let Some(victim) = lru.pop_back() {
77                self.evict(&victim);
78            }
79        }
80    }
81
82    fn evict(&self, namespace: &str) {
83        let mut handles = self.handles.write();
84        if let Some(h) = handles.get_mut(namespace) {
85            h.hydrated = false;
86            self.evictions.fetch_add(1, Ordering::Relaxed);
87        }
88    }
89
90    pub fn evict_idle(&self) {
91        let now = Instant::now();
92        let victims: Vec<String> = self
93            .handles
94            .read()
95            .iter()
96            .filter(|(_, h)| {
97                h.hydrated && now.duration_since(h.last_access) > self.config.idle_evict_after
98            })
99            .map(|(k, _)| k.clone())
100            .collect();
101        for v in victims {
102            self.evict(&v);
103        }
104    }
105
106    pub fn resident_count(&self) -> usize {
107        self.handles.read().values().filter(|h| h.hydrated).count()
108    }
109
110    pub fn eviction_count(&self) -> u64 {
111        self.evictions.load(Ordering::Relaxed)
112    }
113}