Skip to main content

core_crypto_keystore/transaction/
mod.rs

1use std::{
2    borrow::Cow,
3    collections::{HashMap, HashSet, hash_map::Entry},
4    sync::Arc,
5};
6
7use async_lock::{RwLock, SemaphoreGuardArc};
8use itertools::Itertools;
9
10use crate::{
11    CryptoKeystoreError, CryptoKeystoreResult, Database,
12    entities::{MlsPendingMessage, PersistedMlsGroup},
13    traits::{BorrowPrimaryKey, Entity, EntityDatabaseMutation, EntityDeleteBorrowed, KeyType, SearchableEntity},
14    transaction::dynamic_dispatch::EntityId,
15};
16
17pub(crate) mod dynamic_dispatch;
18
19/// table: primary key -> entity reference
20type InMemoryTable = HashMap<EntityId, dynamic_dispatch::Entity>;
21/// collection: collection name -> table
22type InMemoryCollection = Arc<RwLock<HashMap<&'static str, InMemoryTable>>>;
23
24/// This represents a transaction, where all operations will be done in memory and committed at the
25/// end
26#[derive(Debug, Clone)]
27pub struct KeystoreTransaction {
28    cache: InMemoryCollection,
29    deleted: Arc<RwLock<HashSet<EntityId>>>,
30    _semaphore_guard: Arc<SemaphoreGuardArc>,
31}
32
33impl KeystoreTransaction {
34    /// Instantiate a new transaction.
35    ///
36    /// Requires a semaphore guard to ensure that only one exists at a time.
37    pub(crate) async fn new(semaphore_guard: SemaphoreGuardArc) -> CryptoKeystoreResult<Self> {
38        Ok(Self {
39            cache: Default::default(),
40            deleted: Arc::new(Default::default()),
41            _semaphore_guard: Arc::new(semaphore_guard),
42        })
43    }
44
45    /// Save an entity into this transaction.
46    ///
47    /// This is a multi-step process:
48    ///
49    /// - Adjust the entity by calling its [`pre_save()`][Entity::pre_save] method.
50    /// - Store the entity in an internal map.
51    ///   - Remove the entity from the set of deleted entities, if it was there.
52    /// - On [`Self::commit`], actually persist the entity into the supplied database.
53    pub(crate) async fn save<E>(&self, mut entity: E) -> CryptoKeystoreResult<E::AutoGeneratedFields>
54    where
55        E: Entity + EntityDatabaseMutation + Send + Sync,
56    {
57        let auto_generated_fields = entity.pre_save()?;
58
59        let entity_id =
60            EntityId::from_entity(&entity).ok_or(CryptoKeystoreError::UnknownCollectionName(E::COLLECTION_NAME))?;
61        {
62            // start by adding the entity
63            let mut cache_guard = self.cache.write().await;
64            let table = cache_guard.entry(E::COLLECTION_NAME).or_default();
65            table.insert(entity_id.clone(), entity.into());
66        }
67        {
68            // at this point remove the entity from the set of deleted entities to ensure that
69            // this new data gets propagated
70            let mut cache_guard = self.deleted.write().await;
71            cache_guard.remove(&entity_id);
72        }
73
74        Ok(auto_generated_fields)
75    }
76
77    async fn remove_by_entity_id<E>(&self, entity_id: EntityId) -> CryptoKeystoreResult<()>
78    where
79        E: Entity + EntityDatabaseMutation,
80    {
81        // rm this entity from the set of added/modified items
82        // it might never touch the real db at all
83        let mut cache_guard = self.cache.write().await;
84        if let Entry::Occupied(mut table) = cache_guard.entry(E::COLLECTION_NAME)
85            && let Entry::Occupied(cached_record) = table.get_mut().entry(entity_id.clone())
86        {
87            cached_record.remove_entry();
88        };
89
90        // add this entity to the set of items which should be deleted from the persisted db
91        let mut deleted_set = self.deleted.write().await;
92        deleted_set.insert(entity_id);
93        Ok(())
94    }
95
96    /// Remove an entity by its primary key.
97    ///
98    /// Where the primary key has a distinct borrowed form, consider [`Self::remove_borrowed`].
99    ///
100    /// Note that this doesn't return whether or not anything was actually removed because
101    /// that won't happen until the transaction is committed.
102    pub(crate) async fn remove<E>(&self, id: &E::PrimaryKey) -> CryptoKeystoreResult<()>
103    where
104        E: Entity + EntityDatabaseMutation,
105    {
106        let entity_id = EntityId::from_primary_key::<E>(id)
107            .ok_or(CryptoKeystoreError::UnknownCollectionName(E::COLLECTION_NAME))?;
108        self.remove_by_entity_id::<E>(entity_id).await
109    }
110
111    /// Remove an entity by the borrowed form of its primary key.
112    ///
113    /// Note that this doesn't return whether or not anything was actually removed because
114    /// that won't happen until the transaction is committed.
115    pub(crate) async fn remove_borrowed<E>(&self, id: &E::BorrowedPrimaryKey) -> CryptoKeystoreResult<()>
116    where
117        E: EntityDeleteBorrowed + BorrowPrimaryKey,
118    {
119        let entity_id = EntityId::from_borrowed_primary_key::<E>(id)
120            .ok_or(CryptoKeystoreError::UnknownCollectionName(E::COLLECTION_NAME))?;
121        self.remove_by_entity_id::<E>(entity_id).await
122    }
123
124    /// Restore an entity that was deleted in this transaction by removing it from the deleted list. This is
125    /// idempotent: if the entity doesn't exist in the deleted list, do nothing.
126    ///
127    /// NOTE: This will only work if the entity has been added in an earlier transaction, because otherwise,
128    /// removing its id from the deleted list wouldn't suffice: we'd need to replay its insertion.
129    pub(crate) async fn restore<E>(&self, id: &E::BorrowedPrimaryKey) -> CryptoKeystoreResult<()>
130    where
131        E: EntityDeleteBorrowed + BorrowPrimaryKey,
132    {
133        let entity_id = EntityId::from_borrowed_primary_key::<E>(id)
134            .ok_or(CryptoKeystoreError::UnknownCollectionName(E::COLLECTION_NAME))?;
135        let mut deleted = self.deleted.write().await;
136        deleted.remove(&entity_id);
137        Ok(())
138    }
139
140    pub(crate) async fn child_groups(
141        &self,
142        entity: PersistedMlsGroup,
143        persisted_records: impl IntoIterator<Item = PersistedMlsGroup>,
144    ) -> CryptoKeystoreResult<Vec<PersistedMlsGroup>> {
145        // First get all raw groups from the cache, then filter by their parent id
146        let cached_records = self.find_all_in_cache::<PersistedMlsGroup>().await;
147        let cached_records = cached_records
148            .iter()
149            .filter(|maybe_child| {
150                maybe_child
151                    .parent_id
152                    .as_deref()
153                    .map(|parent_id| parent_id == entity.borrow_primary_key().bytes().as_ref())
154                    .unwrap_or_default()
155            })
156            .map(Arc::as_ref)
157            .map(Cow::Borrowed);
158
159        let persisted_records = persisted_records.into_iter().map(Cow::Owned);
160
161        Ok(self.merge_records(cached_records, persisted_records).await)
162    }
163
164    pub(crate) async fn remove_pending_messages_by_conversation_id(&self, conversation_id: impl AsRef<[u8]> + Send) {
165        let conversation_id = conversation_id.as_ref();
166
167        let mut cache_guard = self.cache.write().await;
168        if let Entry::Occupied(mut table) = cache_guard.entry(MlsPendingMessage::COLLECTION_NAME) {
169            table.get_mut().retain(|_key, entity| {
170                let pending_message = entity
171                    .downcast::<MlsPendingMessage>()
172                    .expect("table for MlsPendingMessage contains only that type");
173                pending_message.foreign_id != conversation_id
174            });
175        }
176        drop(cache_guard);
177
178        let mut deleted_set = self.deleted.write().await;
179        deleted_set.insert(
180            EntityId::from_key::<MlsPendingMessage>(conversation_id.into())
181                .expect("mls pending messages are proper entities which can be parsed"),
182        );
183    }
184
185    pub(crate) async fn find_pending_messages_by_conversation_id(
186        &self,
187        conversation_id: &[u8],
188        persisted_records: impl IntoIterator<Item = MlsPendingMessage>,
189    ) -> CryptoKeystoreResult<Vec<MlsPendingMessage>> {
190        let persisted_records = persisted_records.into_iter().map(Cow::Owned);
191
192        let cached_records = self.find_all_in_cache::<MlsPendingMessage>().await;
193        let cached_records = cached_records
194            .iter()
195            .filter(|pending_message| pending_message.foreign_id == conversation_id)
196            .map(Arc::as_ref)
197            .map(Cow::Borrowed);
198
199        let merged_records = self.merge_records(cached_records, persisted_records).await;
200        Ok(merged_records)
201    }
202
203    async fn find_in_cache<E>(&self, entity_id: &EntityId) -> Option<Arc<E>>
204    where
205        E: 'static + Entity + Send + Sync,
206    {
207        let cache_guard = self.cache.read().await;
208        cache_guard
209            .get(E::COLLECTION_NAME)
210            .and_then(|table| table.get(entity_id).and_then(|entity| entity.downcast()))
211    }
212
213    /// The result of this function will have different contents for different scenarios:
214    /// * `Some(Some(E))` - the transaction cache contains the record
215    /// * `Some(None)` - the deletion of the record has been cached
216    /// * `None` - there is no information about the record in the cache
217    async fn get_by_entity_id<E>(&self, entity_id: &EntityId) -> Option<Option<Arc<E>>>
218    where
219        E: 'static + Entity + Send + Sync,
220    {
221        // when applying our transaction to the real database, we delete after inserting,
222        // so here we have to check for deletion before we check for existing values
223        let deleted_list = self.deleted.read().await;
224        if deleted_list.contains(entity_id) {
225            return Some(None);
226        }
227
228        self.find_in_cache::<E>(entity_id).await.map(Some)
229    }
230
231    /// The result of this function will have different contents for different scenarios:
232    /// * `Some(Some(E))` - the transaction cache contains the record
233    /// * `Some(None)` - the deletion of the record has been cached
234    /// * `None` - there is no information about the record in the cache
235    pub(crate) async fn get<E>(&self, id: &E::PrimaryKey) -> Option<Option<Arc<E>>>
236    where
237        E: 'static + Entity + Send + Sync,
238    {
239        let entity_id = EntityId::from_primary_key::<E>(id)?;
240        self.get_by_entity_id(&entity_id).await
241    }
242
243    /// The result of this function will have different contents for different scenarios:
244    /// * `Some(Some(E))` - the transaction cache contains the record
245    /// * `Some(None)` - the deletion of the record has been cached
246    /// * `None` - there is no information about the record in the cache
247    pub(crate) async fn get_borrowed<E>(&self, id: &E::BorrowedPrimaryKey) -> Option<Option<Arc<E>>>
248    where
249        E: 'static + Entity + BorrowPrimaryKey + Send + Sync,
250    {
251        let entity_id = EntityId::from_borrowed_primary_key::<E>(id)?;
252        self.get_by_entity_id(&entity_id).await
253    }
254
255    async fn find_all_in_cache<E>(&self) -> Vec<Arc<E>>
256    where
257        E: 'static + Entity + Send + Sync,
258    {
259        let cache_guard = self.cache.read().await;
260        cache_guard
261            .get(E::COLLECTION_NAME)
262            .map(|table| {
263                table
264                    .values()
265                    .map(|record: &dynamic_dispatch::Entity| {
266                        record
267                            .downcast::<E>()
268                            .expect("all entries in this table are of this type")
269                            .clone()
270                    })
271                    .collect::<Vec<_>>()
272            })
273            .unwrap_or_default()
274    }
275
276    async fn search_in_cache<E, SearchKey>(&self, search_key: &SearchKey) -> Vec<Arc<E>>
277    where
278        E: 'static + Entity + SearchableEntity<SearchKey> + Send + Sync,
279        SearchKey: KeyType,
280    {
281        let cache_guard = self.cache.read().await;
282        cache_guard
283            .get(E::COLLECTION_NAME)
284            .map(|table| {
285                table
286                    .values()
287                    .filter_map(|record: &dynamic_dispatch::Entity| {
288                        let entity = record
289                            .downcast::<E>()
290                            .expect("all entries in this table are of this type")
291                            .clone();
292                        entity.matches(search_key).then_some(entity)
293                    })
294                    .collect()
295            })
296            .unwrap_or_default()
297    }
298
299    pub(crate) async fn find_all<E>(&self, persisted_records: Vec<E>) -> CryptoKeystoreResult<Vec<E>>
300    where
301        E: 'static + Clone + Entity + Send + Sync,
302    {
303        let cached_records = self.find_all_in_cache().await;
304        let merged_records = self
305            .merge_records(
306                cached_records.iter().map(Arc::as_ref).map(Cow::Borrowed),
307                persisted_records.into_iter().map(Cow::Owned),
308            )
309            .await;
310        Ok(merged_records)
311    }
312
313    pub(crate) async fn search<E, SearchKey>(
314        &self,
315        persisted_records: Vec<E>,
316        search_key: &SearchKey,
317    ) -> CryptoKeystoreResult<Vec<E>>
318    where
319        E: 'static + Clone + Entity + SearchableEntity<SearchKey> + Send + Sync,
320        SearchKey: KeyType,
321    {
322        let cached_records = self.search_in_cache(search_key).await;
323        let merged_records = self
324            .merge_records(
325                cached_records.iter().map(Arc::as_ref).map(Cow::Borrowed),
326                persisted_records.into_iter().map(Cow::Owned),
327            )
328            .await;
329        Ok(merged_records)
330    }
331
332    /// Build a single list of unique records from two potentially overlapping lists.
333    /// In case of overlap, records in `records_a` are prioritized.
334    /// Identity from the perspective of this function is determined by the output of
335    /// [Entity::merge_key].
336    ///
337    /// Further, the output list of records is built with respect to the provided [EntityFindParams]
338    /// and the deleted records cached in this [Self] instance.
339    async fn merge_records<'a, E>(
340        &self,
341        records_a: impl IntoIterator<Item = Cow<'a, E>>,
342        records_b: impl IntoIterator<Item = Cow<'a, E>>,
343    ) -> Vec<E>
344    where
345        E: 'static + Clone + Entity,
346    {
347        let deleted_records = self.deleted.read().await;
348
349        records_a
350            .into_iter()
351            .chain(records_b)
352            .unique_by(|e| e.primary_key().bytes().into_owned())
353            .filter_map(|record| {
354                let id = EntityId::from_entity(record.as_ref())?;
355                (!deleted_records.contains(&id)).then_some(record.into_owned())
356            })
357            .collect()
358    }
359
360    /// Persists all the operations in the database. It will effectively open a transaction
361    /// internally, perform all the buffered operations and commit.
362    pub(crate) async fn commit(self, db: &Database) -> Result<(), CryptoKeystoreError> {
363        let cache = self.cache.read().await;
364        let deleted_ids = self.deleted.read().await;
365
366        let table_names_with_deletion = deleted_ids.iter().map(|entity_id| entity_id.collection_name());
367        let table_names_with_save = cache
368            .values()
369            .flat_map(|table| table.keys())
370            .map(|entity_id| entity_id.collection_name());
371        let any_tables_are_modified = table_names_with_deletion.chain(table_names_with_save).next().is_some();
372
373        if !any_tables_are_modified {
374            log::debug!("Empty transaction was committed.");
375            return Ok(());
376        }
377
378        // open a database transaction
379        // Because `rusqlite::Transaction: !Send + !Sync`, it's critical that
380        // we don't hold this transaction over any `.await` points.
381        let mut conn = db.conn().await;
382        let tx = conn.transaction()?;
383
384        for entity in cache.values().flat_map(|table| table.values()) {
385            entity.execute_save(&tx)?;
386        }
387
388        for deleted_id in deleted_ids.iter() {
389            deleted_id.execute_delete(&tx)?;
390        }
391
392        // and commit everything
393        tx.commit()?;
394
395        Ok(())
396    }
397}