1use std::collections::HashMap;
6use std::hash::Hash;
7use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
8use std::sync::{Arc, RwLock};
9use std::time::{Duration, Instant};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct BufferPoolConfig {
14 pub capacity: usize,
16 pub min_block_age_ms: u64,
21}
22
23impl Default for BufferPoolConfig {
24 fn default() -> Self {
25 Self {
26 capacity: 128 * 1024 * 1024,
27 min_block_age_ms: 0,
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct BlockId {
35 pub file_id: u64,
37 pub block_offset: u64,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct DataBlock {
44 bytes: Vec<u8>,
45}
46
47impl DataBlock {
48 pub fn new(bytes: Vec<u8>) -> Self {
50 Self { bytes }
51 }
52
53 pub fn len(&self) -> usize {
55 self.bytes.len()
56 }
57
58 pub fn is_empty(&self) -> bool {
60 self.bytes.is_empty()
61 }
62
63 pub fn as_slice(&self) -> &[u8] {
65 &self.bytes
66 }
67}
68
69#[derive(Debug)]
70struct CacheEntry {
71 block: Arc<DataBlock>,
72 size: usize,
73 inserted_at: Instant,
74}
75
76#[derive(Debug, Default)]
78pub struct BufferPoolStats {
79 hits: AtomicU64,
80 misses: AtomicU64,
81 puts: AtomicU64,
82 evictions: AtomicU64,
83 oversized_puts: AtomicU64,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct BufferPoolStatsSnapshot {
89 pub hits: u64,
91 pub misses: u64,
93 pub puts: u64,
95 pub evictions: u64,
97 pub oversized_puts: u64,
99}
100
101impl BufferPoolStats {
102 fn snapshot(&self) -> BufferPoolStatsSnapshot {
103 BufferPoolStatsSnapshot {
104 hits: self.hits.load(Ordering::Relaxed),
105 misses: self.misses.load(Ordering::Relaxed),
106 puts: self.puts.load(Ordering::Relaxed),
107 evictions: self.evictions.load(Ordering::Relaxed),
108 oversized_puts: self.oversized_puts.load(Ordering::Relaxed),
109 }
110 }
111}
112
113#[derive(Debug)]
115pub struct BufferPool {
116 cache: RwLock<LruCache<BlockId, CacheEntry>>,
117 capacity: usize,
118 min_block_age: Duration,
119 current_size: AtomicUsize,
120 stats: BufferPoolStats,
121}
122
123impl BufferPool {
124 pub fn new(config: BufferPoolConfig) -> Self {
126 Self {
127 cache: RwLock::new(LruCache::new()),
128 capacity: config.capacity,
129 min_block_age: Duration::from_millis(config.min_block_age_ms),
130 current_size: AtomicUsize::new(0),
131 stats: BufferPoolStats::default(),
132 }
133 }
134
135 pub fn current_size_bytes(&self) -> usize {
137 self.current_size.load(Ordering::Relaxed)
138 }
139
140 pub fn capacity_bytes(&self) -> usize {
142 self.capacity
143 }
144
145 pub fn stats(&self) -> BufferPoolStatsSnapshot {
147 self.stats.snapshot()
148 }
149
150 pub fn get(&self, id: &BlockId) -> Option<Arc<DataBlock>> {
152 let mut cache = self.cache.write().expect("poisoned BufferPool lock");
153 match cache.get(id) {
154 Some(entry) => {
155 self.stats.hits.fetch_add(1, Ordering::Relaxed);
156 Some(Arc::clone(&entry.block))
157 }
158 None => {
159 self.stats.misses.fetch_add(1, Ordering::Relaxed);
160 None
161 }
162 }
163 }
164
165 pub fn put(&self, id: BlockId, block: Arc<DataBlock>) -> bool {
171 self.stats.puts.fetch_add(1, Ordering::Relaxed);
172
173 let size = block.len();
174 if size > self.capacity {
175 self.stats.oversized_puts.fetch_add(1, Ordering::Relaxed);
176 return false;
177 }
178
179 let mut cache = self.cache.write().expect("poisoned BufferPool lock");
180 let now = Instant::now();
181
182 if let Some(old) = cache.put(
183 id,
184 CacheEntry {
185 block,
186 size,
187 inserted_at: now,
188 },
189 ) {
190 let old_size = old.size;
191 match size.cmp(&old_size) {
192 std::cmp::Ordering::Greater => {
193 self.current_size
194 .fetch_add(size - old_size, Ordering::Relaxed);
195 }
196 std::cmp::Ordering::Less => {
197 self.current_size
198 .fetch_sub(old_size - size, Ordering::Relaxed);
199 }
200 std::cmp::Ordering::Equal => {}
201 }
202 } else {
203 self.current_size.fetch_add(size, Ordering::Relaxed);
204 }
205
206 self.evict_while_over_capacity_locked(&mut cache, now);
207 true
208 }
209
210 pub fn evict_one(&self) -> Option<(BlockId, Arc<DataBlock>)> {
212 let mut cache = self.cache.write().expect("poisoned BufferPool lock");
213 let now = Instant::now();
214 let evicted = self.evict_one_locked(&mut cache, now, self.min_block_age);
215 if let Some((id, entry)) = evicted {
216 self.current_size.fetch_sub(entry.size, Ordering::Relaxed);
217 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
218 return Some((id, entry.block));
219 }
220 None
221 }
222
223 fn evict_while_over_capacity_locked(
224 &self,
225 cache: &mut LruCache<BlockId, CacheEntry>,
226 now: Instant,
227 ) {
228 while self.current_size.load(Ordering::Relaxed) > self.capacity {
229 let Some((_, entry)) = self.evict_one_locked(cache, now, self.min_block_age) else {
230 break;
231 };
232 self.current_size.fetch_sub(entry.size, Ordering::Relaxed);
233 self.stats.evictions.fetch_add(1, Ordering::Relaxed);
234 }
235 }
236
237 fn evict_one_locked(
238 &self,
239 cache: &mut LruCache<BlockId, CacheEntry>,
240 now: Instant,
241 min_age: Duration,
242 ) -> Option<(BlockId, CacheEntry)> {
243 if cache.len() == 0 {
244 return None;
245 }
246
247 if min_age == Duration::from_millis(0) {
248 return cache.pop_lru();
249 }
250
251 let mut candidate = cache.tail_index();
252 while let Some(index) = candidate {
253 let entry = cache.value_at(index)?;
254 if now.saturating_duration_since(entry.inserted_at) >= min_age {
255 return cache.remove_at(index);
256 }
257 candidate = cache.prev_index(index);
258 }
259
260 cache.pop_lru()
262 }
263}
264
265#[derive(Debug)]
266struct Node<K, V> {
267 key: K,
268 value: V,
269 prev: Option<usize>,
270 next: Option<usize>,
271}
272
273#[derive(Debug)]
274struct LruCache<K, V> {
275 map: HashMap<K, usize>,
276 nodes: Vec<Option<Node<K, V>>>,
277 free: Vec<usize>,
278 head: Option<usize>,
279 tail: Option<usize>,
280 len: usize,
281}
282
283impl<K, V> LruCache<K, V>
284where
285 K: Eq + Hash + Copy,
286{
287 fn new() -> Self {
288 Self {
289 map: HashMap::new(),
290 nodes: Vec::new(),
291 free: Vec::new(),
292 head: None,
293 tail: None,
294 len: 0,
295 }
296 }
297
298 fn len(&self) -> usize {
299 self.len
300 }
301
302 fn tail_index(&self) -> Option<usize> {
303 self.tail
304 }
305
306 fn prev_index(&self, index: usize) -> Option<usize> {
307 self.nodes
308 .get(index)
309 .and_then(|n| n.as_ref().and_then(|n| n.prev))
310 }
311
312 fn value_at(&self, index: usize) -> Option<&V> {
313 self.nodes
314 .get(index)
315 .and_then(|n| n.as_ref().map(|n| &n.value))
316 }
317
318 fn get(&mut self, key: &K) -> Option<&V> {
319 let index = *self.map.get(key)?;
320 self.move_to_front(index);
321 self.value_at(index)
322 }
323
324 fn put(&mut self, key: K, value: V) -> Option<V> {
325 if let Some(&index) = self.map.get(&key) {
326 let old = self
327 .nodes
328 .get_mut(index)
329 .and_then(|n| n.as_mut())
330 .map(|n| std::mem::replace(&mut n.value, value));
331 self.move_to_front(index);
332 return old;
333 }
334
335 let index = self.alloc_index();
336 let node = Node {
337 key,
338 value,
339 prev: None,
340 next: None,
341 };
342 self.nodes[index] = Some(node);
343 self.map.insert(key, index);
344 self.len += 1;
345 self.attach_front(index);
346 None
347 }
348
349 fn pop_lru(&mut self) -> Option<(K, V)> {
350 let index = self.tail?;
351 self.remove_at(index)
352 }
353
354 fn remove_at(&mut self, index: usize) -> Option<(K, V)> {
355 let node = self.nodes.get_mut(index)?.take()?;
356 self.detach(index, node.prev, node.next);
357 self.map.remove(&node.key);
358 self.free.push(index);
359 self.len -= 1;
360 Some((node.key, node.value))
361 }
362
363 fn alloc_index(&mut self) -> usize {
364 if let Some(index) = self.free.pop() {
365 return index;
366 }
367 let index = self.nodes.len();
368 self.nodes.push(None);
369 index
370 }
371
372 fn move_to_front(&mut self, index: usize) {
373 if Some(index) == self.head {
374 return;
375 }
376 let (prev, next) = match self.nodes.get(index).and_then(|n| n.as_ref()) {
377 Some(node) => (node.prev, node.next),
378 None => return,
379 };
380 self.detach(index, prev, next);
381 self.attach_front(index);
382 }
383
384 fn detach(&mut self, index: usize, prev: Option<usize>, next: Option<usize>) {
385 if let Some(prev) = prev {
386 if let Some(Some(node)) = self.nodes.get_mut(prev) {
387 node.next = next;
388 }
389 } else {
390 self.head = next;
391 }
392
393 if let Some(next) = next {
394 if let Some(Some(node)) = self.nodes.get_mut(next) {
395 node.prev = prev;
396 }
397 } else {
398 self.tail = prev;
399 }
400
401 if let Some(Some(node)) = self.nodes.get_mut(index) {
402 node.prev = None;
403 node.next = None;
404 }
405 }
406
407 fn attach_front(&mut self, index: usize) {
408 let old_head = self.head;
409 self.head = Some(index);
410 if let Some(old_head) = old_head {
411 if let Some(Some(node)) = self.nodes.get_mut(old_head) {
412 node.prev = Some(index);
413 }
414 if let Some(Some(node)) = self.nodes.get_mut(index) {
415 node.next = Some(old_head);
416 node.prev = None;
417 }
418 } else {
419 self.tail = Some(index);
420 if let Some(Some(node)) = self.nodes.get_mut(index) {
421 node.prev = None;
422 node.next = None;
423 }
424 }
425 }
426}
427
428#[cfg(all(test, not(target_arch = "wasm32")))]
429mod tests {
430 use super::*;
431
432 fn block(n: u8, len: usize) -> Arc<DataBlock> {
433 Arc::new(DataBlock::new(vec![n; len]))
434 }
435
436 #[test]
437 fn put_get_updates_stats() {
438 let pool = BufferPool::new(BufferPoolConfig {
439 capacity: 1024,
440 min_block_age_ms: 0,
441 });
442
443 let id = BlockId {
444 file_id: 1,
445 block_offset: 0,
446 };
447 assert!(pool.put(id, block(7, 10)));
448
449 assert!(pool.get(&id).is_some());
450 assert!(pool
451 .get(&BlockId {
452 file_id: 1,
453 block_offset: 999,
454 })
455 .is_none());
456
457 let s = pool.stats();
458 assert_eq!(s.hits, 1);
459 assert_eq!(s.misses, 1);
460 assert_eq!(s.puts, 1);
461 }
462
463 #[test]
464 fn lru_evicts_least_recent() {
465 let pool = BufferPool::new(BufferPoolConfig {
466 capacity: 8,
467 min_block_age_ms: 0,
468 });
469
470 let a = BlockId {
471 file_id: 1,
472 block_offset: 0,
473 };
474 let b = BlockId {
475 file_id: 1,
476 block_offset: 1,
477 };
478 let c = BlockId {
479 file_id: 1,
480 block_offset: 2,
481 };
482
483 assert!(pool.put(a, block(1, 4)));
484 assert!(pool.put(b, block(2, 4)));
485
486 assert!(pool.get(&a).is_some());
488 assert!(pool.put(c, block(3, 4)));
489
490 assert!(pool.get(&a).is_some());
491 assert!(pool.get(&b).is_none());
492 assert!(pool.get(&c).is_some());
493 }
494
495 #[test]
496 fn oversized_block_is_not_cached() {
497 let pool = BufferPool::new(BufferPoolConfig {
498 capacity: 4,
499 min_block_age_ms: 0,
500 });
501
502 let id = BlockId {
503 file_id: 1,
504 block_offset: 0,
505 };
506 assert!(!pool.put(id, block(1, 5)));
507 assert_eq!(pool.current_size_bytes(), 0);
508
509 let s = pool.stats();
510 assert_eq!(s.puts, 1);
511 assert_eq!(s.oversized_puts, 1);
512 }
513}