Skip to main content

subms_lsm_tree/features/
block_cache_integration.rs

1//! Read-path block cache wiring.
2//!
3//! The read path consults a [`BlockCache`] before reading from disk. This
4//! module ships:
5//! - [`BlockKey`] - the (sstable_id, block_offset) cache key.
6//! - [`Block`] - an owned byte buffer; `Arc<[u8]>` so the cache can hand the
7//!   same payload to many concurrent readers without copying.
8//! - [`BlockCache`] - the trait the read path calls.
9//! - [`LruBlockCache`] - a working LRU implementation suitable for the LSM
10//!   tree's modest cardinality + single-thread base. A real production
11//!   replacement (TinyLFU, segmented LRU) lives in the sibling recipe
12//!   `subms-block-cache`; this module is the wiring contract, not the
13//!   policy ceiling.
14//!
15//! The base [`crate::LsmTree`] does NOT depend on this module. It's a
16//! standalone trait + reference impl that an integrator wires in via their
17//! own custom read path - mirrors the bloom-filter recipe's relationship.
18
19use std::collections::HashMap;
20use std::sync::Arc;
21
22/// Cache lookup key: (sstable id, block byte offset within the file).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
24pub struct BlockKey {
25    pub sstable_id: u64,
26    pub block_offset: u64,
27}
28
29impl BlockKey {
30    pub fn new(sstable_id: u64, block_offset: u64) -> Self {
31        Self {
32            sstable_id,
33            block_offset,
34        }
35    }
36}
37
38/// Cached block payload. `Arc<[u8]>` so multiple readers see the same bytes
39/// without paying for a copy on lookup.
40pub type Block = Arc<[u8]>;
41
42/// The trait the read path calls. Implementations decide replacement policy,
43/// concurrency, and whether to admit insertions.
44pub trait BlockCache: Send + Sync {
45    /// Returns the cached payload if present.
46    fn get(&self, key: &BlockKey) -> Option<Block>;
47
48    /// Insert a block. May evict to honour capacity bounds.
49    fn put(&self, key: BlockKey, block: Block);
50
51    /// Current number of cached entries.
52    fn len(&self) -> usize;
53
54    /// True if no entries are cached.
55    fn is_empty(&self) -> bool {
56        self.len() == 0
57    }
58
59    /// Drop every entry. Used by tests + manifest swaps.
60    fn clear(&self);
61}
62
63/// Bounded LRU. Uses a doubly-linked list of node indices plus a hashmap for
64/// O(1) lookup. The list is implemented over a `Vec<Node>` to avoid the
65/// allocator overhead of `Box<Node>` per insert.
66///
67/// Single-threaded under the hood; the trait advertises `Sync` via an
68/// internal mutex so the cache can be shared across reader threads.
69pub struct LruBlockCache {
70    inner: std::sync::Mutex<LruInner>,
71}
72
73struct LruInner {
74    capacity: usize,
75    map: HashMap<BlockKey, usize>,
76    nodes: Vec<Node>,
77    head: Option<usize>,
78    tail: Option<usize>,
79    free: Vec<usize>,
80    hits: u64,
81    misses: u64,
82}
83
84struct Node {
85    key: BlockKey,
86    block: Block,
87    prev: Option<usize>,
88    next: Option<usize>,
89}
90
91impl LruBlockCache {
92    pub fn new(capacity: usize) -> Self {
93        let cap = capacity.max(1);
94        Self {
95            inner: std::sync::Mutex::new(LruInner {
96                capacity: cap,
97                map: HashMap::with_capacity(cap),
98                nodes: Vec::with_capacity(cap),
99                head: None,
100                tail: None,
101                free: Vec::new(),
102                hits: 0,
103                misses: 0,
104            }),
105        }
106    }
107
108    pub fn capacity(&self) -> usize {
109        self.inner.lock().unwrap().capacity
110    }
111
112    pub fn hits(&self) -> u64 {
113        self.inner.lock().unwrap().hits
114    }
115
116    pub fn misses(&self) -> u64 {
117        self.inner.lock().unwrap().misses
118    }
119}
120
121impl BlockCache for LruBlockCache {
122    fn get(&self, key: &BlockKey) -> Option<Block> {
123        let mut g = self.inner.lock().unwrap();
124        match g.map.get(key).copied() {
125            Some(idx) => {
126                let block = g.nodes[idx].block.clone();
127                g.move_to_front(idx);
128                g.hits += 1;
129                Some(block)
130            }
131            None => {
132                g.misses += 1;
133                None
134            }
135        }
136    }
137
138    fn put(&self, key: BlockKey, block: Block) {
139        let mut g = self.inner.lock().unwrap();
140        // Already cached: refresh the payload, bump to front, done.
141        if let Some(&idx) = g.map.get(&key) {
142            g.nodes[idx].block = block;
143            g.move_to_front(idx);
144            return;
145        }
146        // Evict if at capacity.
147        if g.map.len() >= g.capacity {
148            if let Some(tail_idx) = g.tail {
149                let tail_key = g.nodes[tail_idx].key;
150                g.detach(tail_idx);
151                g.map.remove(&tail_key);
152                g.free.push(tail_idx);
153            }
154        }
155        // Allocate.
156        let idx = if let Some(slot) = g.free.pop() {
157            g.nodes[slot] = Node {
158                key,
159                block,
160                prev: None,
161                next: None,
162            };
163            slot
164        } else {
165            g.nodes.push(Node {
166                key,
167                block,
168                prev: None,
169                next: None,
170            });
171            g.nodes.len() - 1
172        };
173        g.map.insert(key, idx);
174        g.push_front(idx);
175    }
176
177    fn len(&self) -> usize {
178        self.inner.lock().unwrap().map.len()
179    }
180
181    fn clear(&self) {
182        let mut g = self.inner.lock().unwrap();
183        g.map.clear();
184        g.nodes.clear();
185        g.free.clear();
186        g.head = None;
187        g.tail = None;
188    }
189}
190
191impl LruInner {
192    fn push_front(&mut self, idx: usize) {
193        self.nodes[idx].prev = None;
194        self.nodes[idx].next = self.head;
195        if let Some(h) = self.head {
196            self.nodes[h].prev = Some(idx);
197        }
198        self.head = Some(idx);
199        if self.tail.is_none() {
200            self.tail = Some(idx);
201        }
202    }
203
204    fn detach(&mut self, idx: usize) {
205        let prev = self.nodes[idx].prev;
206        let next = self.nodes[idx].next;
207        if let Some(p) = prev {
208            self.nodes[p].next = next;
209        } else {
210            self.head = next;
211        }
212        if let Some(n) = next {
213            self.nodes[n].prev = prev;
214        } else {
215            self.tail = prev;
216        }
217        self.nodes[idx].prev = None;
218        self.nodes[idx].next = None;
219    }
220
221    fn move_to_front(&mut self, idx: usize) {
222        if self.head == Some(idx) {
223            return;
224        }
225        self.detach(idx);
226        self.push_front(idx);
227    }
228}
229
230#[cfg(test)]
231#[path = "block_cache_integration_tests.rs"]
232mod tests;