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;