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