git_bug/replica/cache/
db.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! Shared code for performant cache lookups and populations.
12//!
13//! The main point of this module is the [`impl_cache`] macro.
14
15/// The Error returned by unsuccessful cache lookups.
16#[derive(Debug, thiserror::Error)]
17#[allow(missing_docs)]
18pub enum Error {
19    #[error("Failed to start a read transaction in the cache db: {0}")]
20    DbRead(Box<redb::TransactionError>),
21
22    #[error("Failed to start a write transaction in the cache db: {0}")]
23    DbWrite(Box<redb::TransactionError>),
24
25    #[error("Failed to open a table in the cache db: {0}")]
26    DbOpenTable(redb::TableError),
27
28    #[error("Failed trying to get a value from the cache db: {0}")]
29    DbGet(redb::StorageError),
30    #[error("Failed trying to insert a value into the cache db: {0}")]
31    DbInsert(redb::StorageError),
32    #[error("Failed trying to remove a stale key value from the cache db: {0}")]
33    DbRemove(redb::StorageError),
34
35    #[error("Failed to commit the cache database write transaction: {0}")]
36    DbWriteCommit(redb::CommitError),
37}
38
39/// Implement caching.
40///
41/// # Example
42/// ```no_run
43/// use git_bug::impl_cache;
44/// use git_bug::replica::Replica;
45///
46/// pub mod compute {
47///     use git_bug::replica::cache;
48///
49///     #[derive(Debug)]
50///     pub enum Error {
51///         CacheError(cache::Error),
52///     }
53///
54///     impl From<cache::Error> for Error {
55///         fn from(value: cache::Error) -> Self {
56///            Self::CacheError(value)
57///         }
58///     }
59///
60///     impl std::fmt::Display for Error {
61///         fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
62///             match self {
63///                 Self::CacheError(err) => err.fmt(f),
64///             }
65///         }
66///     }
67///
68///     impl std::error::Error for Error {}
69/// }
70///
71/// fn computation_heavy_function(replica: &Replica, id: String) -> Result<String, compute::Error> {
72///     impl_cache!(@mk_table "operation_packs");
73///
74///     impl_cache! {@lookup replica.db(), id.as_bytes()}
75///
76///     let me = {
77///         // Some very heavy computation.
78///         String::new()
79///     };
80///
81///     impl_cache! {@populate replica.db(), id.as_bytes(), &me}
82///
83///     Ok(me)
84/// }
85/// ```
86///
87/// You should provide the cache database (`$db`) (probably via
88/// [`Replica::db`][`crate::replica::Replica::db`]) and a unique key (`$key`).
89#[macro_export]
90macro_rules! impl_cache {
91    (@mk_table $table_name:literal) => {
92        const TABLE: redb::TableDefinition<'static, &[u8], &[u8]> =
93            redb::TableDefinition::new($table_name);
94    };
95
96    (@lookup $db:expr, $key:expr) => {{
97        use $crate::replica::cache::Error;
98
99        // Check, whether this operation was already cached.
100        let read_txn = $db
101            .begin_read()
102            .map_err(|err| Error::DbRead(Box::new(err)))?;
103
104        let table = match read_txn.open_table(TABLE) {
105            Ok(val) => Some(val),
106            Err(err) => match &err {
107                redb::TableError::TableDoesNotExist(_) => None,
108                _ => Err(Error::DbOpenTable(err))?,
109            },
110        };
111
112        if let Some(table) = table {
113            if let Some(value) = table.get($key).map_err(Error::DbGet)? {
114                let maybe_me = postcard::from_bytes(value.value());
115
116                match maybe_me {
117                    Ok(me) => return Ok(me),
118                    Err(err) => {
119                        log::error!("Failed to decode a previously cached key: {err}");
120
121                        // Delete the stale key.
122                        let write_txn = $db
123                            .begin_write()
124                            .map_err(|err| Error::DbWrite(Box::new(err)))?;
125
126                        {
127                            let mut table =
128                                write_txn.open_table(TABLE).map_err(Error::DbOpenTable)?;
129
130                            if table.remove($key).map_err(Error::DbRemove)?.is_none() {
131                                log::warn!(
132                                    "Detected racing deletes into the cache db. (Stale key was \
133                                     already deleted)"
134                                );
135                            }
136                        }
137
138                        write_txn.commit().map_err(Error::DbWriteCommit)?;
139                    }
140                }
141            }
142        }
143    }};
144
145    (@populate $db:expr, $key:expr, $value:expr) => {{
146        use $crate::replica::cache::Error;
147
148        // Populate the cache.
149        let write_txn = $db
150            .begin_write()
151            .map_err(|err| Error::DbWrite(Box::new(err)))?;
152
153        {
154            let mut table = write_txn.open_table(TABLE).map_err(Error::DbOpenTable)?;
155
156            // PERF(@bpeetz): This could use a buffer, to avoid allocations. <2025-06-06>
157            let me_data = postcard::to_allocvec($value).expect("Encoding should always work");
158
159            if table
160                .insert($key, me_data.as_slice())
161                .map_err(Error::DbInsert)?
162                .is_some()
163            {
164                log::warn!(
165                    "Detected racing writes into the cache db. (Key insertion attempted, although \
166                     key is already present in cache db)"
167                );
168            }
169        }
170
171        write_txn.commit().map_err(Error::DbWriteCommit)?;
172    }};
173}
174
175pub(crate) use impl_cache;