Skip to main content

persistent_queue/
store.rs

1//! The [`Store`] trait and its built-in backends.
2
3use std::error::Error as StdError;
4
5/// A single write in a [`Store::commit`] batch.
6pub enum Op<'a> {
7    /// Insert or overwrite `key` with `value`.
8    Put(&'a [u8], &'a [u8]),
9    /// Remove `key`.
10    Delete(&'a [u8]),
11}
12
13/// A key/value pair returned by a store seek.
14pub type KeyValue = (Vec<u8>, Vec<u8>);
15
16/// An ordered key/value byte store: the durable substrate under the queue.
17///
18/// Keys sort lexicographically. The queue needs forward and backward seeks and
19/// one atomic, optionally durable, batch write; it never asks the store to know
20/// anything about queues.
21pub trait Store: Send + Sync {
22    /// The backend's error type.
23    type Error: StdError + Send + Sync + 'static;
24
25    /// Value for an exact key.
26    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error>;
27
28    /// Smallest entry whose key is `>= from`.
29    fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error>;
30
31    /// Greatest entry whose key is `<= upto`.
32    fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error>;
33
34    /// Apply `ops`. When `durable`, do not return until they survive a crash.
35    fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error>;
36}
37
38// ---- mem ----
39
40/// In-memory [`Store`] backed by a `BTreeMap`. Not persistent; `durable` is a
41/// no-op. Useful as the default, for tests, and as a benchmark baseline.
42pub struct MemStore {
43    map: crate::sync::Mutex<std::collections::BTreeMap<Vec<u8>, Vec<u8>>>,
44}
45
46impl MemStore {
47    /// Create an empty store.
48    pub fn new() -> Self {
49        Self {
50            map: crate::sync::Mutex::new(std::collections::BTreeMap::new()),
51        }
52    }
53}
54
55impl Default for MemStore {
56    fn default() -> Self {
57        Self::new()
58    }
59}
60
61impl Store for MemStore {
62    type Error = std::convert::Infallible;
63
64    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
65        Ok(self.map.lock().unwrap().get(key).cloned())
66    }
67
68    fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
69        Ok(self
70            .map
71            .lock()
72            .unwrap()
73            .range(from.to_vec()..)
74            .next()
75            .map(|(k, v)| (k.clone(), v.clone())))
76    }
77
78    fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
79        Ok(self
80            .map
81            .lock()
82            .unwrap()
83            .range(..=upto.to_vec())
84            .next_back()
85            .map(|(k, v)| (k.clone(), v.clone())))
86    }
87
88    fn commit(&self, ops: &[Op<'_>], _durable: bool) -> Result<(), Self::Error> {
89        let mut map = self.map.lock().unwrap();
90        for op in ops {
91            match op {
92                Op::Put(k, v) => {
93                    map.insert(k.to_vec(), v.to_vec());
94                }
95                Op::Delete(k) => {
96                    map.remove(*k);
97                }
98            }
99        }
100        Ok(())
101    }
102}
103
104// ---- sled ----
105
106/// [`Store`] backed by a [sled](https://docs.rs/sled) database. Requires the
107/// `sled` feature.
108///
109/// Only one process may open a given database directory at a time; sled takes an
110/// exclusive lock. A backend error on open, including a corrupt store, is surfaced
111/// by [`Builder::open`](crate::Builder::open) as
112/// [`OpenError::Store`](crate::OpenError::Store); the queue does not auto-repair.
113#[cfg(feature = "sled")]
114pub struct SledStore {
115    db: sled::Db,
116}
117
118#[cfg(feature = "sled")]
119impl SledStore {
120    /// Open (creating if needed) a sled database at `path`.
121    pub fn open(path: impl AsRef<std::path::Path>) -> sled::Result<Self> {
122        Ok(Self {
123            db: sled::open(path)?,
124        })
125    }
126
127    /// Wrap an already-open sled database.
128    pub fn from_db(db: sled::Db) -> Self {
129        Self { db }
130    }
131}
132
133#[cfg(feature = "sled")]
134impl Store for SledStore {
135    type Error = sled::Error;
136
137    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
138        Ok(self.db.get(key)?.map(|v| v.to_vec()))
139    }
140
141    fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
142        match self.db.range(from.to_vec()..).next() {
143            Some(r) => {
144                let (k, v) = r?;
145                Ok(Some((k.to_vec(), v.to_vec())))
146            }
147            None => Ok(None),
148        }
149    }
150
151    fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
152        match self.db.range(..=upto.to_vec()).next_back() {
153            Some(r) => {
154                let (k, v) = r?;
155                Ok(Some((k.to_vec(), v.to_vec())))
156            }
157            None => Ok(None),
158        }
159    }
160
161    fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
162        let mut batch = sled::Batch::default();
163        for op in ops {
164            match op {
165                Op::Put(k, v) => batch.insert(*k, *v),
166                Op::Delete(k) => batch.remove(*k),
167            }
168        }
169        self.db.apply_batch(batch)?;
170        if durable {
171            self.db.flush()?;
172        }
173        Ok(())
174    }
175}
176
177// ---- redb ----
178
179// redb 3.0 moved `begin_read` to the `ReadableDatabase` trait.
180#[cfg(feature = "redb")]
181use redb::ReadableDatabase;
182
183#[cfg(feature = "redb")]
184const REDB_TABLE: redb::TableDefinition<'static, &[u8], &[u8]> =
185    redb::TableDefinition::new("entries");
186
187/// [`Store`] backed by a [redb](https://docs.rs/redb) database. Requires the
188/// `redb` feature.
189///
190/// Only one process may open a given database file at a time; redb takes an
191/// exclusive lock. A backend error on open, including a corrupt store, is surfaced
192/// by [`Builder::open`](crate::Builder::open) as
193/// [`OpenError::Store`](crate::OpenError::Store); the queue does not auto-repair.
194#[cfg(feature = "redb")]
195pub struct RedbStore {
196    db: redb::Database,
197}
198
199#[cfg(feature = "redb")]
200impl RedbStore {
201    /// Open (creating if needed) a redb database at `path`.
202    // The Store trait surfaces redb::Error unboxed, so keep open consistent.
203    #[allow(clippy::result_large_err)]
204    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, redb::Error> {
205        let db = redb::Database::create(path)?;
206        let wtx = db.begin_write()?;
207        wtx.open_table(REDB_TABLE)?;
208        wtx.commit()?;
209        Ok(Self { db })
210    }
211
212    /// Wrap an already-open redb database.
213    pub fn from_db(db: redb::Database) -> Self {
214        Self { db }
215    }
216}
217
218#[cfg(feature = "redb")]
219impl Store for RedbStore {
220    type Error = redb::Error;
221
222    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
223        let rtx = self.db.begin_read()?;
224        let table = rtx.open_table(REDB_TABLE)?;
225        Ok(table.get(key)?.map(|g| g.value().to_vec()))
226    }
227
228    fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
229        let rtx = self.db.begin_read()?;
230        let table = rtx.open_table(REDB_TABLE)?;
231        match table.range::<&[u8]>(from..)?.next() {
232            Some(r) => {
233                let (k, v) = r?;
234                Ok(Some((k.value().to_vec(), v.value().to_vec())))
235            }
236            None => Ok(None),
237        }
238    }
239
240    fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
241        let rtx = self.db.begin_read()?;
242        let table = rtx.open_table(REDB_TABLE)?;
243        match table.range::<&[u8]>(..=upto)?.next_back() {
244            Some(r) => {
245                let (k, v) = r?;
246                Ok(Some((k.value().to_vec(), v.value().to_vec())))
247            }
248            None => Ok(None),
249        }
250    }
251
252    fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
253        let mut wtx = self.db.begin_write()?;
254        if !durable {
255            // redb 3.0 made `set_durability` fallible.
256            wtx.set_durability(redb::Durability::None)?;
257        }
258        {
259            let mut table = wtx.open_table(REDB_TABLE)?;
260            for op in ops {
261                match op {
262                    Op::Put(k, v) => {
263                        table.insert(*k, *v)?;
264                    }
265                    Op::Delete(k) => {
266                        table.remove(*k)?;
267                    }
268                }
269            }
270        }
271        wtx.commit()?;
272        Ok(())
273    }
274}
275
276// ---- rocksdb ----
277
278/// [`Store`] backed by [RocksDB](https://docs.rs/rocksdb). Requires the `rocksdb`
279/// feature, which builds a bundled C++ RocksDB (needs a C++ toolchain).
280///
281/// Only one process may open a given database directory at a time; RocksDB takes an
282/// exclusive lock. A backend error on open, including a corrupt store, is surfaced
283/// by [`Builder::open`](crate::Builder::open) as
284/// [`OpenError::Store`](crate::OpenError::Store); the queue does not auto-repair.
285///
286/// The bundled RocksDB is compiled from source and statically linked into your binary
287/// (nothing extra to ship), but it needs a C++ toolchain to build, adds compile time
288/// and binary size, and links the C++ runtime - so a fully static (musl) binary is
289/// hard. For a small, pure-Rust, fully static binary, use `sled` or `redb`.
290#[cfg(feature = "rocksdb")]
291pub struct RocksStore {
292    db: rocksdb::DB,
293}
294
295#[cfg(feature = "rocksdb")]
296impl RocksStore {
297    /// Open (creating if needed) a RocksDB database at `path`.
298    pub fn open(path: impl AsRef<std::path::Path>) -> Result<Self, rocksdb::Error> {
299        Ok(Self {
300            db: rocksdb::DB::open_default(path)?,
301        })
302    }
303
304    /// Wrap an already-open RocksDB database.
305    pub fn from_db(db: rocksdb::DB) -> Self {
306        Self { db }
307    }
308}
309
310#[cfg(feature = "rocksdb")]
311impl Store for RocksStore {
312    type Error = rocksdb::Error;
313
314    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, Self::Error> {
315        self.db.get(key)
316    }
317
318    fn seek(&self, from: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
319        let mut iter = self.db.iterator(rocksdb::IteratorMode::From(
320            from,
321            rocksdb::Direction::Forward,
322        ));
323        match iter.next() {
324            Some(Ok((k, v))) => Ok(Some((k.to_vec(), v.to_vec()))),
325            Some(Err(e)) => Err(e),
326            None => Ok(None),
327        }
328    }
329
330    fn seek_back(&self, upto: &[u8]) -> Result<Option<KeyValue>, Self::Error> {
331        let mut iter = self.db.iterator(rocksdb::IteratorMode::From(
332            upto,
333            rocksdb::Direction::Reverse,
334        ));
335        match iter.next() {
336            Some(Ok((k, v))) => Ok(Some((k.to_vec(), v.to_vec()))),
337            Some(Err(e)) => Err(e),
338            None => Ok(None),
339        }
340    }
341
342    fn commit(&self, ops: &[Op<'_>], durable: bool) -> Result<(), Self::Error> {
343        let mut batch = rocksdb::WriteBatch::default();
344        for op in ops {
345            match op {
346                Op::Put(k, v) => batch.put(*k, *v),
347                Op::Delete(k) => batch.delete(*k),
348            }
349        }
350        let mut opts = rocksdb::WriteOptions::default();
351        opts.set_sync(durable);
352        self.db.write_opt(batch, &opts)
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn mem_store_contract() {
362        contract(MemStore::new());
363    }
364
365    #[cfg(feature = "sled")]
366    #[test]
367    fn sled_store_contract() {
368        let dir = tempfile::tempdir().unwrap();
369        contract(SledStore::open(dir.path().join("db")).unwrap());
370    }
371
372    #[cfg(feature = "redb")]
373    #[test]
374    fn redb_store_contract() {
375        let dir = tempfile::tempdir().unwrap();
376        contract(RedbStore::open(dir.path().join("db.redb")).unwrap());
377    }
378
379    #[cfg(feature = "rocksdb")]
380    #[test]
381    fn rocksdb_store_contract() {
382        let dir = tempfile::tempdir().unwrap();
383        contract(RocksStore::open(dir.path().join("db")).unwrap());
384    }
385
386    // A store reads keys back exactly, keeps them in byte-lexicographic order, and
387    // applies deletes; seeks find the nearest key in each direction.
388    fn contract<S: Store>(store: S) {
389        assert!(store.get(b"missing").unwrap().is_none());
390        assert!(store.seek(b"a").unwrap().is_none());
391
392        store
393            .commit(
394                &[
395                    Op::Put(b"b", b"2"),
396                    Op::Put(b"a", b"1"),
397                    Op::Put(b"c", b"3"),
398                ],
399                true,
400            )
401            .unwrap();
402
403        assert_eq!(store.get(b"a").unwrap().as_deref(), Some(&b"1"[..]));
404        assert_eq!(store.get(b"z").unwrap(), None);
405
406        let (k, v) = store.seek(b"a").unwrap().unwrap();
407        assert_eq!((k.as_slice(), v.as_slice()), (&b"a"[..], &b"1"[..]));
408        assert_eq!(store.seek(b"aa").unwrap().unwrap().0.as_slice(), b"b");
409        assert_eq!(store.seek_back(b"bz").unwrap().unwrap().0.as_slice(), b"b");
410        assert_eq!(
411            store.seek_back(b"\xff").unwrap().unwrap().0.as_slice(),
412            b"c"
413        );
414
415        store.commit(&[Op::Delete(b"b")], true).unwrap();
416        assert_eq!(store.get(b"b").unwrap(), None);
417        assert_eq!(store.seek(b"b").unwrap().unwrap().0.as_slice(), b"c");
418    }
419}