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}