/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 | | } |