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}