Skip to main content

spacedb_store/
mem_engine.rs

1//! In-memory [`KvEngine`] — the test/ephemeral engine.
2//!
3//! Its entire reason to exist is to back unit tests and any non-durable path, so
4//! it must reproduce the **transactional semantics** of the real engine exactly —
5//! not just store bytes. In particular a write transaction here **buffers**
6//! mutations in an overlay and applies them atomically on commit; dropping it
7//! discards the overlay (rollback). A naïve "mutate the map directly" impl would
8//! pass functional tests while silently failing the atomicity/rollback tests,
9//! defeating the point of having two engines.
10//!
11//! Concurrency mirrors the single-writer model via an `RwLock`: a write txn holds
12//! the write guard for its lifetime (so no reader observes a half-applied txn),
13//! and read txns hold a read guard (a consistent snapshot for their lifetime).
14
15use std::collections::BTreeMap;
16use std::ops::Bound;
17use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
18
19use crate::engine::{Durability, KvEngine, ReadTx, Readable, WriteTx};
20use crate::error::{StoreError, StoreResult};
21
22type Table = BTreeMap<Vec<u8>, Vec<u8>>;
23type Tables = BTreeMap<String, Table>;
24
25/// An in-memory, transactional key/value engine. Loses all data on drop.
26#[derive(Default)]
27pub struct MemEngine {
28    tables: RwLock<Tables>,
29}
30
31impl MemEngine {
32    pub fn new() -> Self {
33        Self::default()
34    }
35}
36
37fn poisoned() -> StoreError {
38    StoreError::engine("in-memory lock poisoned")
39}
40
41/// Collect the `[lo, hi)` slice of a table into sorted `(key, value)` pairs.
42fn range_table(table: &Table, lo: &[u8], hi: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
43    table
44        .range::<[u8], _>((Bound::Included(lo), Bound::Excluded(hi)))
45        .map(|(k, v)| (k.clone(), v.clone()))
46        .collect()
47}
48
49// ─── read transaction ────────────────────────────────────────────────────────
50
51/// A consistent read snapshot held for the transaction's lifetime.
52pub struct MemReadTx<'a> {
53    guard: RwLockReadGuard<'a, Tables>,
54}
55
56impl Readable for MemReadTx<'_> {
57    fn get_raw(&self, table: &str, key: &[u8]) -> StoreResult<Option<Vec<u8>>> {
58        Ok(self.guard.get(table).and_then(|t| t.get(key).cloned()))
59    }
60
61    fn range_raw(&self, table: &str, lo: &[u8], hi: &[u8]) -> StoreResult<Vec<(Vec<u8>, Vec<u8>)>> {
62        Ok(self
63            .guard
64            .get(table)
65            .map(|t| range_table(t, lo, hi))
66            .unwrap_or_default())
67    }
68}
69
70impl ReadTx for MemReadTx<'_> {}
71
72// ─── write transaction ───────────────────────────────────────────────────────
73
74/// `Some(bytes)` = put this value; `None` = delete this key. Buffered until commit.
75type Overlay = BTreeMap<String, BTreeMap<Vec<u8>, Option<Vec<u8>>>>;
76
77/// A single-writer transaction. Holds the write guard (single-writer) and buffers
78/// mutations in `overlay`; `commit` applies them to the base map, `drop` discards.
79pub struct MemWriteTx<'a> {
80    base: RwLockWriteGuard<'a, Tables>,
81    overlay: Overlay,
82    _durability: Durability,
83}
84
85impl Readable for MemWriteTx<'_> {
86    fn get_raw(&self, table: &str, key: &[u8]) -> StoreResult<Option<Vec<u8>>> {
87        // Read-your-own-writes: the overlay shadows the base map.
88        if let Some(t) = self.overlay.get(table) {
89            if let Some(slot) = t.get(key) {
90                return Ok(slot.clone());
91            }
92        }
93        Ok(self.base.get(table).and_then(|t| t.get(key).cloned()))
94    }
95
96    fn range_raw(&self, table: &str, lo: &[u8], hi: &[u8]) -> StoreResult<Vec<(Vec<u8>, Vec<u8>)>> {
97        // Materialize the base slice, then apply this txn's overlay edits within
98        // the range so a range scan also sees uncommitted writes.
99        let mut merged: BTreeMap<Vec<u8>, Vec<u8>> = self
100            .base
101            .get(table)
102            .map(|t| range_table(t, lo, hi).into_iter().collect())
103            .unwrap_or_default();
104        if let Some(t) = self.overlay.get(table) {
105            for (k, slot) in t.range::<[u8], _>((Bound::Included(lo), Bound::Excluded(hi))) {
106                match slot {
107                    Some(v) => {
108                        merged.insert(k.clone(), v.clone());
109                    }
110                    None => {
111                        merged.remove(k);
112                    }
113                }
114            }
115        }
116        Ok(merged.into_iter().collect())
117    }
118}
119
120impl WriteTx for MemWriteTx<'_> {
121    fn put_raw(&mut self, table: &str, key: &[u8], val: &[u8]) -> StoreResult<()> {
122        self.overlay
123            .entry(table.to_string())
124            .or_default()
125            .insert(key.to_vec(), Some(val.to_vec()));
126        Ok(())
127    }
128
129    fn delete_raw(&mut self, table: &str, key: &[u8]) -> StoreResult<bool> {
130        let existed = self.get_raw(table, key)?.is_some();
131        self.overlay
132            .entry(table.to_string())
133            .or_default()
134            .insert(key.to_vec(), None);
135        Ok(existed)
136    }
137
138    fn commit(mut self) -> StoreResult<()> {
139        // Apply atomically under the held write guard. Taking `overlay` by value
140        // makes the post-commit state explicit and avoids cloning.
141        let overlay = std::mem::take(&mut self.overlay);
142        for (table, edits) in overlay {
143            let t = self.base.entry(table).or_default();
144            for (key, slot) in edits {
145                match slot {
146                    Some(v) => {
147                        t.insert(key, v);
148                    }
149                    None => {
150                        t.remove(&key);
151                    }
152                }
153            }
154        }
155        Ok(())
156    }
157}
158
159// ─── engine ──────────────────────────────────────────────────────────────────
160
161impl KvEngine for MemEngine {
162    type RTx<'a> = MemReadTx<'a>;
163    type WTx<'a> = MemWriteTx<'a>;
164
165    fn begin_read(&self) -> StoreResult<Self::RTx<'_>> {
166        let guard = self.tables.read().map_err(|_| poisoned())?;
167        Ok(MemReadTx { guard })
168    }
169
170    fn begin_write(&self, durability: Durability) -> StoreResult<Self::WTx<'_>> {
171        let base = self.tables.write().map_err(|_| poisoned())?;
172        Ok(MemWriteTx {
173            base,
174            overlay: Overlay::new(),
175            _durability: durability,
176        })
177    }
178}