Coverage Report

Created: 2026-07-11 22:33

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