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}