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(super::managed_connection::Guarded<MutexGuard<'a, super::ManagedConnection>>),
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 let inner = self.conn.lock().await;
103 super::managed_connection::Guarded {
104 inner,
105 _sqlite: super::SqliteGuard::lock(),
106 }
107 .into()
108 }
109 }
110
111 /// Ensure a transaction exists, passing it to the operation.
112 ///
113 /// If a transaction is already in progress, perform the operation and pass on its result
114 /// without affecting the transaction lifecycle at all, whether or not the operation succeeded.
115 ///
116 /// NOTE: if the transaction was already in progress, its owner's `.commit()` will block until
117 /// the operation here has completed.
118 ///
119 /// If a transaction was _not_ already in progress, create one and then perform the operation.
120 /// If the operation succeeded, commit the transaction; otherwise, roll it back.
121 /// Then return the result.
122 ///
123 /// If the operation succeeded but the commit failed, the commit error masks the operation's
124 /// success.
125 ///
126 /// Because the operation and therefore this overall function can return an arbitrary error type,
127 /// and there is not necessarily a direct relation between [`CryptoKeystoreError`] and `E`,
128 /// this function requires an explicit mapping function to be provided.
129 ///
130 /// NOTE: Because of TOCTOU, there is an interval between when we check if an existing
131 /// transaction exists, and when we create our own. If a separate process creates a transaction
132 /// during that interval, then this function must wait for that external transaction to
133 /// complete before it can acquire the transaction lock. This might be surprising, but isn't
134 /// worth putting in the effort to change; it's not strictly a bug.
135 pub async fn ensure_transaction<T, E>(
136 self: &Arc<Self>,
137 operation: impl AsyncFnOnce(&Transaction) -> Result<T, E>,
138 map_err: impl Fn(CryptoKeystoreError) -> E,
139 ) -> Result<T, E> {
140 #[derive(derive_more::Deref, derive_more::From)]
141 enum TxHandle {
142 /// Someone else created this transaction and we have a guard over it
143 Borrowed(#[deref(forward)] ArcWithReadGuard<Transaction>),
144 /// We created this transaction and need to commit it ourselves
145 Owned(#[deref(forward)] UniqueArc<Transaction>),
146 }
147
148 // don't keep the lock alive, to prevent deadlocks
149 let weak = (*self.transaction.lock().await).clone();
150 // still waiting on `Option::async_map`, `Option::async_and_then`
151 let existing = match weak {
152 Some(weak) => weak.upgrade_without_type_erasure().await,
153 None => None,
154 };
155 let handle: TxHandle = match existing {
156 Some(existing) => existing.into(),
157 None => self.new_transaction().await.map_err(&map_err)?.into(),
158 };
159
160 let result = operation(&handle).await;
161
162 if let TxHandle::Owned(tx) = handle {
163 if result.is_ok() {
164 tx.commit().await.map_err(&map_err)?;
165 } else {
166 tx.rollback().await.map_err(&map_err)?;
167 }
168 }
169
170 result
171 }
172}