Skip to main content

core_crypto_keystore/transaction/
mod.rs

1mod conn;
2mod fetch_from_database;
3mod finalize;
4mod mls;
5#[cfg(feature = "proteus-keystore")]
6mod proteus;
7mod savepoint;
8
9use std::sync::Arc;
10
11use async_lock::MutexGuardArc;
12use rusqlite::Connection;
13
14pub(crate) use self::{conn::TransactionConnection, mls::read_mls_entity};
15use crate::{CryptoKeystoreResult, Database, UniqueArc, connection::TransactionGuard};
16
17const GUARD_EXPECTATION: &str = "connection guard is present for the lifetime of the transaction wrapper";
18
19/// This is a guard over an in-flight transaction.
20///
21/// In a perfect world we'd be able to use [`rusqlite::Transaction`], but that type
22/// is intentionally `!Send + !Sync`, which prevents us from being able to keep a
23/// long-lived transaction around.
24///
25/// Dropping the transaction without committing performs an implicit rollback.
26///
27/// This type is always wrapped in a [`UniqueArc`], which keeps things efficient,
28/// at the cost of prohibiting `Clone`. In case you need to share this around,
29/// there are weak references available via [`UniqueArc::downgrade`].
30/// Alternately, wrap the entire thing in an `Arc<Mutex<Option<UniqueArc<Self>>>>` or similar.
31/// Just be aware that you'll need to take the unique arc out in order to commit.
32pub struct Transaction {
33    /// The lock guard is never used directly, but is held to ensure that only one transaction
34    /// is ever live simultaneously. Sqlite already gives us that protection, but we can `.await`
35    /// the transaction guard instead of just catching the error if we try to double-up.
36    _lock_guard: TransactionGuard,
37    /// The database reference is kept only to invalidate the weak pointer when this transaction
38    /// is either committed or rolled back.
39    ///
40    /// **IMPORTANT**: do not attempt to take `self.database.conn().await`. THIS WILL DEADLOCK.
41    /// Use `self.conn` instead.
42    ///
43    /// We have to hold a separate long-lived lock to the database's internal connection because
44    /// the mutex guarding it is async, but we depend on non-async `Drop` behavior.
45    database: Arc<Database>,
46    /// Guard over the actual database connection. We have to hold this for the entire lifetime
47    /// of this transaction. That's a bit selfish on one hand, because it opens the door to
48    /// potential deadlocks if we write things wrong. On the other hand it is required because
49    /// our `Drop` impl would otherwise need to asynchronously lock the database to get the connection,
50    /// and async `Drop` is not yet a thing.
51    ///
52    /// The `Option` ensures we can safely invalidate this transaction after commit and rollback.
53    /// Can't mess with the database after either of those!
54    ///
55    /// The synchronous Mutex here does a few things:
56    ///
57    /// - turns `Connection: Send + !Sync` -> `Transaction: Send + Sync`
58    /// - by using the synchronous version, the compiler ensures we don't hold a guard over an await point, which would
59    ///   deadlock everything
60    /// - ensures that no two threads race on `conn.prepare` / `prepare_cached`
61    ///
62    /// On Wasm, callers acquire `SqliteGuard` before this mutex: JSPI can suspend
63    /// a synchronous SQLite call while it holds the mutex.
64    conn: Arc<parking_lot::Mutex<Option<MutexGuardArc<crate::connection::ManagedConnection>>>>,
65}
66
67impl Transaction {
68    /// Instantiate a new transaction.
69    ///
70    /// Requires a transaction lock guard to ensure that only one exists at a time.
71    pub(crate) async fn new(
72        lock_guard: TransactionGuard,
73        database: Arc<Database>,
74    ) -> CryptoKeystoreResult<UniqueArc<Self>> {
75        let conn = database.raw_conn().await;
76
77        {
78            let _sqlite = crate::connection::SqliteGuard::lock();
79            // initialize the DB-level transaction before doing any construction work on the type-level
80            // transaction; failure here invalidates everything to follow. We don't need to worry about
81            // concurrency; we already hold the lock guard.
82            let mut stmt = conn.prepare_cached("BEGIN IMMEDIATE TRANSACTION")?;
83            stmt.execute([])?;
84        }
85
86        let transaction = UniqueArc::from(Self {
87            _lock_guard: lock_guard,
88            database,
89            conn: Arc::new(parking_lot::Mutex::new(Some(conn))),
90        });
91
92        let weak = UniqueArc::downgrade(&transaction);
93
94        {
95            let mut transaction_guard = transaction.database.transaction.lock().await;
96            // this transaction guard may be `None` if the database is new or the previous transaction
97            // was committed.
98            // it may be `Some(_)` if the previous transaction was rolled back by means of dropping the
99            // `UniqueArc<Transaction>`. either way, it's correct to simply replace it without checking the
100            // previous value.
101            *transaction_guard = Some(weak);
102        }
103
104        Ok(transaction)
105    }
106
107    /// `true` when the transaction is active.
108    ///
109    /// You'd think that given the presence of this wrapper, you could assume that a transaction
110    /// is active: instantiating one creates a transaction, and the consuming methods close it.
111    /// However, Sqlite can sometimes automatically roll back transactions without telling the
112    /// user:
113    ///
114    /// <https://sqlite.org/c3ref/get_autocommit.html>
115    ///
116    /// > The only way to find out whether SQLite automatically rolled back the transaction
117    /// > after an error is to use this function.
118    fn is_active(conn: &Connection) -> bool {
119        !conn.is_autocommit()
120    }
121}