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}