Skip to main content

alopex_core/lsm/
buffer_pool.rs

1//! SSTable データブロック向けのバッファプール(LRU キャッシュ)。
2//!
3//! 目的: SSTable のデータブロックをメモリに保持して、ディスク I/O を削減する(設計書 §3.5)。
4
5use 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/// バッファプール設定(設計書 §3.5.3)。
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct BufferPoolConfig {
14    /// キャッシュ容量(バイト)。
15    pub capacity: usize,
16    /// 最小ブロック保持時間(ミリ秒)。
17    ///
18    /// 容量超過でエビクションが必要な場合、LRU の末尾からこの保持時間を満たす候補を探索する。
19    /// ただし候補が見つからない場合は、容量制限を優先して LRU 末尾をエビクトする(ベストエフォート)。
20    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/// SSTable ブロックを一意に識別する ID(設計書 §3.5.2)。
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub struct BlockId {
35    /// SSTable ファイル ID。
36    pub file_id: u64,
37    /// ブロックのファイル内オフセット。
38    pub block_offset: u64,
39}
40
41/// キャッシュ対象のデータブロック(バイト列)。
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct DataBlock {
44    bytes: Vec<u8>,
45}
46
47impl DataBlock {
48    /// 新しいデータブロックを作成する。
49    pub fn new(bytes: Vec<u8>) -> Self {
50        Self { bytes }
51    }
52
53    /// ブロックの長さ(バイト)。
54    pub fn len(&self) -> usize {
55        self.bytes.len()
56    }
57
58    /// ブロックが空かどうか。
59    pub fn is_empty(&self) -> bool {
60        self.bytes.is_empty()
61    }
62
63    /// ブロックのバイト列を参照する。
64    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/// バッファプール統計(スレッドセーフ)。
77#[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/// `BufferPoolStats` のスナップショット。
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct BufferPoolStatsSnapshot {
89    /// キャッシュヒット数。
90    pub hits: u64,
91    /// キャッシュミス数。
92    pub misses: u64,
93    /// put 呼び出し回数(キャッシュ可能なサイズに限らない)。
94    pub puts: u64,
95    /// エビクション回数。
96    pub evictions: u64,
97    /// 容量を超えるためキャッシュしなかった put 回数。
98    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/// SSTable データブロック用の LRU バッファプール。
114#[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    /// 新しい `BufferPool` を作成する。
125    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    /// 現在の使用量(バイト、概算ではなく実測)。
136    pub fn current_size_bytes(&self) -> usize {
137        self.current_size.load(Ordering::Relaxed)
138    }
139
140    /// 容量(バイト)。
141    pub fn capacity_bytes(&self) -> usize {
142        self.capacity
143    }
144
145    /// 統計を取得する。
146    pub fn stats(&self) -> BufferPoolStatsSnapshot {
147        self.stats.snapshot()
148    }
149
150    /// ブロックを取得する(ヒット時は LRU を更新)。
151    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    /// ブロックを追加する(容量超過時は LRU でエビクション)。
166    ///
167    /// 戻り値:
168    /// - `true`: キャッシュに格納した
169    /// - `false`: ブロックが容量より大きく、キャッシュしなかった
170    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    /// 明示的に LRU で 1 件エビクトする(エビクトできなければ `None`)。
211    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        // 候補が見つからなければ、容量制限を優先して LRU 末尾をエビクトする(ベストエフォート)。
261        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        // A を最近使ったことにする → 次の put では B が追い出される。
487        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}