Skip to main content

feoxdb/core/
cache.rs

1use crate::constants::*;
2use crate::core::record::Record;
3use crate::stats::Statistics;
4use crate::utils::hash::murmur3_32;
5use bytes::Bytes;
6use parking_lot::{Mutex, RwLock};
7use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
8use std::sync::Arc;
9
10/// CLOCK algorithm cache implementation
11/// Uses reference bits and circular scanning for eviction
12pub struct ClockCache {
13    /// Cache entries organized in buckets for better locality
14    buckets: Vec<RwLock<Vec<CacheEntry>>>,
15
16    /// Global CLOCK hand position for eviction scanning
17    clock_hand: AtomicUsize,
18
19    /// High watermark for triggering eviction (bytes)
20    high_watermark: AtomicUsize,
21
22    /// Low watermark to evict down to (bytes)
23    low_watermark: AtomicUsize,
24
25    /// Lock for eviction process
26    eviction_lock: Mutex<()>,
27
28    /// Shared statistics
29    stats: Arc<Statistics>,
30}
31
32#[derive(Clone)]
33struct CacheEntry {
34    key: Vec<u8>,
35    value: Bytes,
36    record: Option<Arc<Record>>,
37
38    /// Reference bit for CLOCK algorithm (accessed recently)
39    reference_bit: Arc<AtomicBool>,
40
41    /// Size of this entry in bytes
42    size: usize,
43
44    /// Access count for statistics
45    access_count: Arc<AtomicU32>,
46}
47
48impl ClockCache {
49    pub fn new(stats: Arc<Statistics>) -> Self {
50        let buckets = (0..CACHE_BUCKETS)
51            .map(|_| RwLock::new(Vec::new()))
52            .collect();
53
54        Self {
55            buckets,
56            clock_hand: AtomicUsize::new(0),
57            high_watermark: AtomicUsize::new(CACHE_HIGH_WATERMARK_MB * MB),
58            low_watermark: AtomicUsize::new(CACHE_LOW_WATERMARK_MB * MB),
59            eviction_lock: Mutex::new(()),
60            stats,
61        }
62    }
63
64    /// Get value from cache, setting reference bit on access
65    pub fn get(&self, key: &[u8]) -> Option<Bytes> {
66        self.get_entry(key, None)
67    }
68
69    pub(crate) fn get_for_record(&self, key: &[u8], record: &Arc<Record>) -> Option<Bytes> {
70        self.get_entry(key, Some(record))
71    }
72
73    fn get_entry(&self, key: &[u8], record: Option<&Arc<Record>>) -> Option<Bytes> {
74        let hash = murmur3_32(key, 0);
75        let bucket_idx = (hash as usize) % CACHE_BUCKETS;
76
77        let bucket = self.buckets[bucket_idx].read();
78
79        for entry in bucket.iter() {
80            let generation_matches = match (record, entry.record.as_ref()) {
81                (Some(expected), Some(cached)) => Arc::ptr_eq(cached, expected),
82                (Some(_), None) => false,
83                (None, _) => true,
84            };
85            if entry.key == key && generation_matches {
86                // Set reference bit on access (CLOCK algorithm)
87                entry.reference_bit.store(true, Ordering::Release);
88                entry.access_count.fetch_add(1, Ordering::Relaxed);
89                return Some(entry.value.clone());
90            }
91        }
92
93        None
94    }
95
96    /// Insert value into cache, triggering eviction if needed
97    pub fn insert(&self, key: Vec<u8>, value: Bytes) {
98        self.insert_entry(key, value, None);
99    }
100
101    pub(crate) fn insert_for_record(&self, key: Vec<u8>, value: Bytes, record: Arc<Record>) {
102        self.insert_entry(key, value, Some(record));
103    }
104
105    fn insert_entry(&self, key: Vec<u8>, value: Bytes, record: Option<Arc<Record>>) {
106        let size = key.len() + value.len() + std::mem::size_of::<CacheEntry>();
107
108        // Don't cache very large values
109        let high_watermark = self.high_watermark.load(Ordering::Acquire);
110        if size > high_watermark / 4 {
111            return;
112        }
113
114        // Check if we need to evict before inserting
115        let current_usage = self.stats.cache_memory.load(Ordering::Acquire);
116        let high_watermark = self.high_watermark.load(Ordering::Acquire);
117        if current_usage + size > high_watermark {
118            self.evict_entries();
119        }
120
121        let hash = murmur3_32(&key, 0);
122        let bucket_idx = (hash as usize) % CACHE_BUCKETS;
123
124        let mut bucket = self.buckets[bucket_idx].write();
125
126        // Check if key already exists and update
127        for entry in bucket.iter_mut() {
128            if entry.key == key {
129                let old_size = entry.size;
130                entry.value = value;
131                entry.record = record;
132                entry.size = size;
133                entry.reference_bit.store(true, Ordering::Release);
134
135                // Update memory usage
136                if size > old_size {
137                    self.stats
138                        .cache_memory
139                        .fetch_add(size - old_size, Ordering::AcqRel);
140                } else {
141                    self.stats
142                        .cache_memory
143                        .fetch_sub(old_size - size, Ordering::AcqRel);
144                }
145                return;
146            }
147        }
148
149        // Add new entry
150        let entry = CacheEntry {
151            key,
152            value,
153            record,
154            reference_bit: Arc::new(AtomicBool::new(true)),
155            size,
156            access_count: Arc::new(AtomicU32::new(1)),
157        };
158
159        bucket.push(entry);
160        self.stats.cache_memory.fetch_add(size, Ordering::AcqRel);
161    }
162
163    /// Remove specific key from cache
164    pub fn remove(&self, key: &[u8]) {
165        let hash = murmur3_32(key, 0);
166        let bucket_idx = (hash as usize) % CACHE_BUCKETS;
167
168        let mut bucket = self.buckets[bucket_idx].write();
169
170        if let Some(pos) = bucket.iter().position(|e| e.key == key) {
171            let entry = bucket.remove(pos);
172            self.stats
173                .cache_memory
174                .fetch_sub(entry.size, Ordering::AcqRel);
175        }
176    }
177
178    /// CLOCK algorithm eviction - scan entries circularly, evicting those without reference bit
179    pub fn evict_entries(&self) {
180        // Try to acquire eviction lock, return if already evicting
181        let _lock = match self.eviction_lock.try_lock() {
182            Some(lock) => lock,
183            None => return,
184        };
185
186        let target_usage = self.low_watermark.load(Ordering::Acquire);
187        let mut current_usage = self.stats.cache_memory.load(Ordering::Acquire);
188
189        if current_usage <= target_usage {
190            return;
191        }
192
193        let mut scans = 0;
194        const MAX_SCANS: usize = 3; // Maximum passes through cache
195
196        while current_usage > target_usage && scans < MAX_SCANS {
197            let mut entries_checked = 0;
198            let mut bucket_count = 0;
199            for bucket in &self.buckets {
200                bucket_count += bucket.read().len();
201            }
202            let total_entries = bucket_count;
203
204            // Scan through buckets using CLOCK hand
205            while entries_checked < total_entries && current_usage > target_usage {
206                let hand = self.clock_hand.fetch_add(1, Ordering::AcqRel) % CACHE_BUCKETS;
207
208                let mut bucket = self.buckets[hand].write();
209                let mut i = 0;
210
211                while i < bucket.len() {
212                    let entry = &bucket[i];
213
214                    // Check reference bit
215                    if entry.reference_bit.load(Ordering::Acquire) {
216                        // Clear reference bit and give second chance with barrier
217                        entry.reference_bit.store(false, Ordering::Release);
218                        std::sync::atomic::fence(Ordering::Release);
219                        i += 1;
220                    } else {
221                        // No reference bit - evict this entry
222                        let removed = bucket.remove(i);
223                        self.stats
224                            .cache_memory
225                            .fetch_sub(removed.size, Ordering::AcqRel);
226                        self.stats.record_eviction(1);
227                        current_usage -= removed.size;
228                        // Don't increment i since we removed an element
229                    }
230
231                    entries_checked += 1;
232
233                    if current_usage <= target_usage {
234                        break;
235                    }
236                }
237            }
238
239            scans += 1;
240        }
241    }
242
243    /// Clear all cache entries
244    pub fn clear(&self) {
245        for bucket in &self.buckets {
246            bucket.write().clear();
247        }
248
249        self.stats.cache_memory.store(0, Ordering::Release);
250        self.clock_hand.store(0, Ordering::Release);
251    }
252
253    /// Get current cache statistics
254    pub fn stats(&self) -> CacheStats {
255        CacheStats {
256            entries: 0, // Calculate from buckets if needed
257            memory_usage: self.stats.cache_memory.load(Ordering::Acquire),
258            high_watermark: self.high_watermark.load(Ordering::Acquire),
259            low_watermark: self.low_watermark.load(Ordering::Acquire),
260        }
261    }
262
263    /// Adjust cache watermarks dynamically
264    pub fn adjust_watermarks(&self, high_mb: usize, low_mb: usize) {
265        let high = high_mb * MB;
266        let low = low_mb * MB;
267
268        if high > low && high <= CACHE_MAX_SIZE {
269            // Max 1GB for cache
270            // Update watermarks atomically
271            self.high_watermark.store(high, Ordering::Release);
272            self.low_watermark.store(low, Ordering::Release);
273
274            // Trigger eviction if we're over the new high watermark
275            let current_usage = self.stats.cache_memory.load(Ordering::Acquire);
276            if current_usage > high {
277                self.evict_entries();
278            }
279        }
280    }
281}
282
283#[derive(Debug, Clone)]
284pub struct CacheStats {
285    pub entries: u32,
286    pub memory_usage: usize,
287    pub high_watermark: usize,
288    pub low_watermark: usize,
289}