Skip to main content

core_crypto_keystore/transaction/
mod.rs

1mod bulk_delete_filter;
2mod dynamic_dispatch;
3mod entity_read;
4mod entity_write;
5mod fetch_from_database;
6mod mls;
7mod operations;
8#[cfg(feature = "proteus-keystore")]
9pub mod proteus;
10mod read_outcome;
11mod specializations;
12
13use std::{collections::HashMap, sync::Arc};
14
15use async_lock::RwLock;
16use rusqlite::TransactionBehavior;
17
18pub use self::dynamic_dispatch::EntityId;
19pub(crate) use self::{bulk_delete_filter::BulkDeleteFilter, read_outcome::ReadOutcome};
20use self::{dynamic_dispatch::Operation, operations::Operations};
21use crate::{
22    CryptoKeystoreError, CryptoKeystoreResult, Database, UniqueArc, connection::TransactionGuard, traits::Entity,
23};
24
25/// This is an in-flight transaction: all operations are buffered in memory, and only
26/// applied to the database on [`commit`][UniqueArc<Self>::commit].
27///
28/// Dropping the transaction without committing performs an implicit rollback.
29///
30/// This type is always wrapped in a [`UniqueArc`], which keeps things efficient,
31/// at the cost of prohibiting `Clone`. In case you need to share this around,
32/// there are weak references available via [`UniqueArc::downgrade`].
33/// Alternately, wrap the entire thing in an `Arc<Mutex<Option<UniqueArc<Self>>>>` or similar.
34/// Just be aware that you'll need to take the unique arc out in order to commit.
35pub struct Transaction {
36    operations: RwLock<Operations>,
37    _lock_guard: TransactionGuard,
38    database: Arc<Database>,
39}
40
41impl Transaction {
42    /// Instantiate a new transaction.
43    ///
44    /// Requires a transaction lock guard to ensure that only one exists at a time.
45    pub(crate) async fn new(
46        lock_guard: TransactionGuard,
47        database: Arc<Database>,
48    ) -> CryptoKeystoreResult<UniqueArc<Self>> {
49        let transaction = UniqueArc::from(Self {
50            operations: Default::default(),
51            _lock_guard: lock_guard,
52            database,
53        });
54
55        let weak = UniqueArc::downgrade(&transaction);
56
57        {
58            let mut transaction_guard = transaction.database.transaction.lock().await;
59            // this transaction guard may be `None` if the database is new or the previous transaction
60            // was committed.
61            // it may be `Some(_)` if the previous transaction was rolled back by means of dropping the
62            // `UniqueArc<Transaction>`. either way, it's correct to simply replace it without checking the
63            // previous value.
64            *transaction_guard = Some(weak);
65        }
66
67        Ok(transaction)
68    }
69
70    /// Merge the database's view of entity records with entities from the transaction cache.
71    ///
72    /// Entities deleted singly or in bulk from the operations list are excluded, unless later re-added.
73    ///
74    /// Entities upserted in the tx cache overwrite entities from the database.
75    ///
76    /// Note that the position of each deletion within the transaction is not consulted here, unlike
77    /// in the cache-only reads. Every record in `from_database` predates the whole transaction, so any
78    /// deletion in it applies; and "unless later re-added" needs no ordering either, because a
79    /// re-added entity arrives through `from_tx_cache`, which is applied afterwards and therefore
80    /// wins. That leaves the two sources responsible for different halves of the answer:
81    /// `from_tx_cache` is expected to have resolved ordering among the operations already, which is
82    /// what [`Self::find_all_in_cache`] does for it.
83    ///
84    /// The returned order is unspecified, as the merge runs through a `HashMap`.
85    async fn merge_records<E>(
86        &self,
87        from_tx_cache: impl IntoIterator<Item = Arc<E>>,
88        from_database: impl IntoIterator<Item = Arc<E>>,
89    ) -> impl Iterator<Item = Arc<E>>
90    where
91        E: 'static + Clone + Entity + Send + Sync,
92    {
93        let mut cache = {
94            let operations = self.operations.read().await;
95            let filters = operations.bulk_delete_filters();
96
97            // construct the cache from the database's items,
98            // filtering out those items which have been deleted individually or in bulk
99            from_database
100                .into_iter()
101                .filter_map(|entity| {
102                    let entity_id = EntityId::from_entity(&*entity);
103
104                    // the delete may have been overwritten by a later upsert, true,
105                    // but in that case we lose nothing by deleting here, because we
106                    // are still about to upsert a few lines from now
107                    //
108                    // bulk-deletes apply to the database entities regardless of when they happen
109                    let excluded =
110                        operations.last_delete_idx_for(&entity_id).is_some() || filters.applies_after(&*entity, 0);
111                    (!excluded).then_some((entity_id, entity))
112                })
113                .collect::<HashMap<_, _>>()
114        };
115
116        // update with everything which was inserted by the tx cache
117        for entity in from_tx_cache {
118            let id = EntityId::from_entity(&*entity);
119            cache.insert(id, entity);
120        }
121
122        cache.into_values()
123    }
124}
125
126impl UniqueArc<Transaction> {
127    /// Persists all the operations in the database. It will effectively open a transaction
128    /// internally, perform all the buffered operations and commit.
129    pub async fn commit(self) -> Result<(), CryptoKeystoreError> {
130        let Transaction {
131            operations,
132            database,
133            _lock_guard,
134        } = UniqueArc::into_inner(self).await;
135        let operations = operations.into_inner();
136
137        // clear the weak reference to this transaction
138        *database.transaction.lock().await = None;
139
140        if operations.is_empty() {
141            log::debug!("Empty transaction was committed.");
142            return Ok(());
143        }
144
145        // open a database transaction
146        // Because `rusqlite::Transaction: !Send + !Sync`, it's critical that
147        // we don't hold this transaction over any `.await` points.
148        let mut conn = database.conn().await;
149        let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
150
151        for operation in operations {
152            operation.apply(&tx)?;
153        }
154
155        // and commit everything
156        tx.commit()?;
157
158        Ok(())
159    }
160}