Skip to main content

core_crypto_keystore/connection/
transaction.rs

1//! The methods in this module handle keystore transactions.
2//!
3//! Keystore transactions are "fake", in-memory persistence of database operations over time.
4//! They're required because actual [`rusqlite::Transaction`] is `!Send + !Sync`, and we need
5//! `Send` at a minimum in order to keep the transaction around and manipulate it concurrently
6//! from various tasks.
7
8use std::{ops::Deref, sync::Arc};
9
10use async_lock::MutexGuard;
11use rusqlite::Connection;
12
13use crate::{
14    CryptoKeystoreError, CryptoKeystoreResult, Database, Transaction, UniqueArc, transaction::TransactionConnection,
15    unique_arc::ArcWithReadGuard,
16};
17
18/// These impls control the keystore transaction lifecycle.
19impl Database {
20    /// Waits for the current transaction to be committed or rolled back, then starts a new one.
21    ///
22    /// With the `cross-process-lock` feature enabled, this also waits on transactions running
23    /// against the same database in other processes.
24    pub async fn new_transaction(self: &Arc<Self>) -> CryptoKeystoreResult<UniqueArc<Transaction>> {
25        let lock_guard = self.transaction_lock.acquire().await?;
26        Transaction::new(lock_guard, self.clone()).await
27    }
28
29    /// Start a new transaction if no other transaction is currently in progress.
30    ///
31    /// If a transaction is currently in progress, this will produce a `TransactionInProgress` error.
32    /// With the `cross-process-lock` feature enabled, a transaction in progress in another process
33    /// counts too.
34    pub async fn try_new_immediate_transaction(self: &Arc<Self>) -> CryptoKeystoreResult<UniqueArc<Transaction>> {
35        let lock_guard = self
36            .transaction_lock
37            .try_acquire()?
38            .ok_or(CryptoKeystoreError::TransactionInProgress)?;
39        Transaction::new(lock_guard, self.clone()).await
40    }
41
42    /// Do an operation on a new keystore transaction on this database.
43    ///
44    /// This is a convenience method abstracting over the transaction lifecycle;
45    /// it creates a new transaction (including waiting for any existing transaction to finish),
46    /// then performs its operation.
47    ///
48    /// If the operation succeeds, the transaction is committed.
49    /// Otherwise, it is rolled back.
50    pub async fn transactionally<R>(
51        self: &Arc<Self>,
52        operation: impl AsyncFnOnce(&Transaction) -> CryptoKeystoreResult<R>,
53    ) -> CryptoKeystoreResult<R> {
54        let lock_guard = self.transaction_lock.acquire().await?;
55        let transaction = Transaction::new(lock_guard, self.clone()).await?;
56
57        let result = operation(&transaction).await;
58        if result.is_ok() {
59            transaction.commit().await?;
60        } else {
61            transaction.rollback().await?;
62        }
63
64        result
65    }
66
67    /// The connection on which to perform a database operation.
68    ///
69    /// When a transaction is in flight this is tha transaction's connection.
70    /// Otherwise it is the database's internal connection.
71    ///
72    /// It is the caller's responsibility to ensure that mutating operations are only performed
73    /// via a transaction!
74    ///
75    /// The returned type is deliberately `!Send` because there is an inner lock
76    /// which is synchronous (for good reason, see field-level docs on `Transaction::conn`);
77    /// holding that lock across an await point would wedge the database.
78    pub(crate) async fn conn<'a>(&'a self) -> impl 'a + Deref<Target = Connection> {
79        #[derive(derive_more::From)]
80        enum ConnectionGuard<'a> {
81            Transaction(TransactionConnection),
82            Database(MutexGuard<'a, Connection>),
83        }
84
85        impl<'a> Deref for ConnectionGuard<'a> {
86            type Target = Connection;
87
88            fn deref(&self) -> &Self::Target {
89                match self {
90                    ConnectionGuard::Transaction(transaction_connection) => transaction_connection.deref(),
91                    ConnectionGuard::Database(mutex_guard) => mutex_guard.deref(),
92                }
93            }
94        }
95
96        if let Some(weak) = self.transaction.lock().await.clone()
97            && let Some(wrapper) = weak.upgrade_without_type_erasure().await
98            && let Ok(transaction_connection) = TransactionConnection::shared(wrapper)
99        {
100            ConnectionGuard::from(transaction_connection)
101        } else {
102            self.conn.lock().await.into()
103        }
104    }
105
106    /// Ensure a transaction exists, passing it to the operation.
107    ///
108    /// If a transaction is already in progress, perform the operation and pass on its result
109    /// without affecting the transaction lifecycle at all, whether or not the operation succeeded.
110    ///
111    /// NOTE: if the transaction was already in progress, its owner's `.commit()` will block until
112    /// the operation here has completed.
113    ///
114    /// If a transaction was _not_ already in progress, create one and then perform the operation.
115    /// If the operation succeeded, commit the transaction; otherwise, roll it back.
116    /// Then return the result.
117    ///
118    /// If the operation succeeded but the commit failed, the commit error masks the operation's
119    /// success.
120    ///
121    /// Because the operation and therefore this overall function can return an arbitrary error type,
122    /// and there is not necessarily a direct relation between [`CryptoKeystoreError`] and `E`,
123    /// this function requires an explicit mapping function to be provided.
124    ///
125    /// NOTE: Because of TOCTOU, there is an interval between when we check if an existing
126    /// transaction exists, and when we create our own. If a separate process creates a transaction
127    /// during that interval, then this function must wait for that external transaction to
128    /// complete before it can acquire the transaction lock. This might be surprising, but isn't
129    /// worth putting in the effort to change; it's not strictly a bug.
130    pub async fn ensure_transaction<T, E>(
131        self: &Arc<Self>,
132        operation: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
133        map_err: impl Fn(CryptoKeystoreError) -> E,
134    ) -> Result<T, E> {
135        #[derive(derive_more::Deref, derive_more::From)]
136        enum TxHandle {
137            /// Someone else created this transaction and we have a guard over it
138            Borrowed(#[deref(forward)] ArcWithReadGuard<Transaction>),
139            /// We created this transaction and need to commit it ourselves
140            Owned(#[deref(forward)] UniqueArc<Transaction>),
141        }
142
143        // don't keep the lock alive, to prevent deadlocks
144        let weak = (*self.transaction.lock().await).clone();
145        // still waiting on `Option::async_map`, `Option::async_and_then`
146        let existing = match weak {
147            Some(weak) => weak.upgrade_without_type_erasure().await,
148            None => None,
149        };
150        let handle: TxHandle = match existing {
151            Some(existing) => existing.into(),
152            None => self.new_transaction().await.map_err(&map_err)?.into(),
153        };
154
155        let result = operation(&handle).await;
156
157        if let TxHandle::Owned(tx) = handle {
158            if result.is_ok() {
159                tx.commit().await.map_err(&map_err)?;
160            } else {
161                tx.rollback().await.map_err(&map_err)?;
162            }
163        }
164
165        result
166    }
167}