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