git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! Shared code for performant cache lookups and populations.
//!
//! The main point of this module is the [`impl_cache`] macro.

/// The Error returned by unsuccessful cache lookups.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
    #[error("Failed to start a read transaction in the cache db: {0}")]
    DbRead(Box<redb::TransactionError>),

    #[error("Failed to start a write transaction in the cache db: {0}")]
    DbWrite(Box<redb::TransactionError>),

    #[error("Failed to open a table in the cache db: {0}")]
    DbOpenTable(redb::TableError),

    #[error("Failed trying to get a value from the cache db: {0}")]
    DbGet(redb::StorageError),
    #[error("Failed trying to insert a value into the cache db: {0}")]
    DbInsert(redb::StorageError),
    #[error("Failed trying to remove a stale key value from the cache db: {0}")]
    DbRemove(redb::StorageError),

    #[error("Failed to commit the cache database write transaction: {0}")]
    DbWriteCommit(redb::CommitError),
}

/// Implement caching.
///
/// # Example
/// ```no_run
/// use git_bug::impl_cache;
/// use git_bug::replica::Replica;
///
/// pub mod compute {
///     use git_bug::replica::cache;
///
///     #[derive(Debug)]
///     pub enum Error {
///         CacheError(cache::Error),
///     }
///
///     impl From<cache::Error> for Error {
///         fn from(value: cache::Error) -> Self {
///            Self::CacheError(value)
///         }
///     }
///
///     impl std::fmt::Display for Error {
///         fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
///             match self {
///                 Self::CacheError(err) => err.fmt(f),
///             }
///         }
///     }
///
///     impl std::error::Error for Error {}
/// }
///
/// fn computation_heavy_function(replica: &Replica, id: String) -> Result<String, compute::Error> {
///     impl_cache!(@mk_table "operation_packs");
///
///     impl_cache! {@lookup replica.db(), id.as_bytes()}
///
///     let me = {
///         // Some very heavy computation.
///         String::new()
///     };
///
///     impl_cache! {@populate replica.db(), id.as_bytes(), &me}
///
///     Ok(me)
/// }
/// ```
///
/// You should provide the cache database (`$db`) (probably via
/// [`Replica::db`][`crate::replica::Replica::db`]) and a unique key (`$key`).
#[macro_export]
macro_rules! impl_cache {
    (@mk_table $table_name:literal) => {
        const TABLE: redb::TableDefinition<'static, &[u8], &[u8]> =
            redb::TableDefinition::new($table_name);
    };

    (@lookup $db:expr, $key:expr) => {{
        use $crate::replica::cache::Error;

        // Check, whether this operation was already cached.
        let read_txn = $db
            .begin_read()
            .map_err(|err| Error::DbRead(Box::new(err)))?;

        let table = match read_txn.open_table(TABLE) {
            Ok(val) => Some(val),
            Err(err) => match &err {
                redb::TableError::TableDoesNotExist(_) => None,
                _ => Err(Error::DbOpenTable(err))?,
            },
        };

        if let Some(table) = table {
            if let Some(value) = table.get($key).map_err(Error::DbGet)? {
                let maybe_me = postcard::from_bytes(value.value());

                match maybe_me {
                    Ok(me) => return Ok(me),
                    Err(err) => {
                        log::error!("Failed to decode a previously cached key: {err}");

                        // Delete the stale key.
                        let write_txn = $db
                            .begin_write()
                            .map_err(|err| Error::DbWrite(Box::new(err)))?;

                        {
                            let mut table =
                                write_txn.open_table(TABLE).map_err(Error::DbOpenTable)?;

                            if table.remove($key).map_err(Error::DbRemove)?.is_none() {
                                log::warn!(
                                    "Detected racing deletes into the cache db. (Stale key was \
                                     already deleted)"
                                );
                            }
                        }

                        write_txn.commit().map_err(Error::DbWriteCommit)?;
                    }
                }
            }
        }
    }};

    (@populate $db:expr, $key:expr, $value:expr) => {{
        use $crate::replica::cache::Error;

        // Populate the cache.
        let write_txn = $db
            .begin_write()
            .map_err(|err| Error::DbWrite(Box::new(err)))?;

        {
            let mut table = write_txn.open_table(TABLE).map_err(Error::DbOpenTable)?;

            // PERF(@bpeetz): This could use a buffer, to avoid allocations. <2025-06-06>
            let me_data = postcard::to_allocvec($value).expect("Encoding should always work");

            if table
                .insert($key, me_data.as_slice())
                .map_err(Error::DbInsert)?
                .is_some()
            {
                log::warn!(
                    "Detected racing writes into the cache db. (Key insertion attempted, although \
                     key is already present in cache db)"
                );
            }
        }

        write_txn.commit().map_err(Error::DbWriteCommit)?;
    }};
}

pub(crate) use impl_cache;