Skip to main content

fedimint_db_locked/
lib.rs

1use std::path::{Path, PathBuf};
2
3use fedimint_core::db::IRawDatabase;
4use fedimint_core::{apply, async_trait_maybe_send};
5use fedimint_logging::LOG_DB;
6use tracing::{debug, info};
7
8/// Locked version of database
9///
10/// This will use file-system advisory locks to prevent to
11/// serialize opening and using the `DB`.
12///
13/// Use [`LockedBuilder`] to create.
14#[derive(Debug)]
15pub struct Locked<DB> {
16    inner: DB,
17    #[allow(dead_code)] // only for `Drop`
18    lock: fs_lock::FileLock,
19}
20
21/// Builder for [`Locked`]
22pub struct LockedBuilder {
23    lock: fs_lock::FileLock,
24}
25
26impl LockedBuilder {
27    /// Create a [`Self`] by acquiring a lock file
28    pub fn new(db_path: &Path) -> Result<LockedBuilder, DbLockError> {
29        let lock_path = db_path.with_extension("db.lock");
30        let file = std::fs::OpenOptions::new()
31            .write(true)
32            .create(true)
33            .truncate(true)
34            .open(&lock_path)
35            .map_err(|source| DbLockError::Open {
36                path: lock_path.clone(),
37                source,
38            })?;
39
40        debug!(target: LOG_DB, lock=%lock_path.display(), "Acquiring database lock");
41
42        let lock = match fs_lock::FileLock::new_try_exclusive(file) {
43            Ok(lock) => lock,
44            Err((file, _)) => {
45                info!(target: LOG_DB, lock=%lock_path.display(), "Waiting for the database lock");
46
47                fs_lock::FileLock::new_exclusive(file).map_err(DbLockError::Acquire)?
48            }
49        };
50        debug!(target: LOG_DB, lock=%lock_path.display(), "Acquired database lock");
51
52        Ok(LockedBuilder { lock })
53    }
54
55    /// Create [`Locked`] by giving it the database to wrap; fails with the
56    /// error of `db_fn`.
57    pub fn with_db<DB, E>(self, db_fn: impl FnOnce() -> Result<DB, E>) -> Result<Locked<DB>, E> {
58        Ok(Locked {
59            inner: db_fn()?,
60            lock: self.lock,
61        })
62    }
63}
64
65/// Why [`LockedBuilder::new`] could not lock a database.
66#[derive(Debug, thiserror::Error)]
67#[non_exhaustive]
68pub enum DbLockError {
69    /// The lock file next to the database could not be opened or created.
70    #[error("Failed to open {}", path.display())]
71    Open {
72        /// The path of the lock file.
73        path: PathBuf,
74        /// Why it could not be opened.
75        #[source]
76        source: std::io::Error,
77    },
78
79    /// Waiting for the exclusive lock on the lock file failed.
80    #[error("Failed to acquire a lock file")]
81    Acquire(#[source] std::io::Error),
82}
83
84#[apply(async_trait_maybe_send!)]
85impl<DB> IRawDatabase for Locked<DB>
86where
87    DB: IRawDatabase,
88{
89    type Transaction<'a> = DB::Transaction<'a>;
90
91    async fn begin_transaction<'a>(
92        &'a self,
93    ) -> <Locked<DB> as fedimint_core::db::IRawDatabase>::Transaction<'_> {
94        self.inner.begin_transaction().await
95    }
96
97    fn checkpoint(&self, backup_path: &Path) -> fedimint_core::db::DatabaseResult<()> {
98        self.inner.checkpoint(backup_path)
99    }
100}