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` -> `TransactionWrapper: 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    conn: Arc<parking_lot::Mutex<Option<MutexGuardArc<Connection>>>>,
62}
63
64impl Transaction {
65    /// Instantiate a new transaction.
66    ///
67    /// Requires a transaction lock guard to ensure that only one exists at a time.
68    pub(crate) async fn new(
69        lock_guard: TransactionGuard,
70        database: Arc<Database>,
71    ) -> CryptoKeystoreResult<UniqueArc<Self>> {
72        let conn = database.raw_conn().await;
73
74        {
75            // initialize the DB-level transaction before doing any construction work on the type-level
76            // transaction; failure here invalidates everything to follow. We don't need to worry about
77            // concurrency; we already hold the lock guard.
78            let mut stmt = conn.prepare_cached("BEGIN IMMEDIATE TRANSACTION")?;
79            stmt.execute([])?;
80        }
81
82        let transaction = UniqueArc::from(Self {
83            _lock_guard: lock_guard,
84            database,
85            conn: Arc::new(parking_lot::Mutex::new(Some(conn))),
86        });
87
88        let weak = UniqueArc::downgrade(&transaction);
89
90        {
91            let mut transaction_guard = transaction.database.transaction.lock().await;
92            // this transaction guard may be `None` if the database is new or the previous transaction
93            // was committed.
94            // it may be `Some(_)` if the previous transaction was rolled back by means of dropping the
95            // `UniqueArc<Transaction>`. either way, it's correct to simply replace it without checking the
96            // previous value.
97            *transaction_guard = Some(weak);
98        }
99
100        Ok(transaction)
101    }
102
103    /// `true` when the transaction is active.
104    ///
105    /// You'd think that given the presence of this wrapper, you could assume that a transaction
106    /// is active: instantiating one creates a transaction, and the consuming methods close it.
107    /// However, Sqlite can sometimes automatically roll back transactions without telling the
108    /// user:
109    ///
110    /// <https://sqlite.org/c3ref/get_autocommit.html>
111    ///
112    /// > The only way to find out whether SQLite automatically rolled back the transaction
113    /// > after an error is to use this function.
114    fn is_active(conn: &Connection) -> bool {
115        !conn.is_autocommit()
116    }
117}