diff --git a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs index 1adcd318c7a..40329522028 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/integration_tests.rs @@ -1408,57 +1408,57 @@ macro_rules! event_cache_store_integration_tests_time { let store = get_event_cache_store().await.unwrap().into_event_cache_store(); let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired0); + assert_eq!(acquired0, Some(1)); // first lock generation // Should extend the lease automatically (same holder). let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired2); + assert_eq!(acquired2, Some(1)); // same lock generation // Should extend the lease automatically (same holder + time is ok). let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired3); + assert_eq!(acquired3, Some(1)); // same lock generation // Another attempt at taking the lock should fail, because it's taken. let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired4); + assert!(acquired4.is_none()); // not acquired // Even if we insist. let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired5); + assert!(acquired5.is_none()); // not acquired // That's a nice test we got here, go take a little nap. sleep(Duration::from_millis(50)).await; // Still too early. let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired55); + assert!(acquired55.is_none()); // not acquired // Ok you can take another nap then. sleep(Duration::from_millis(250)).await; // At some point, we do get the lock. let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap(); - assert!(acquired6); + assert_eq!(acquired6, Some(2)); // new lock generation! sleep(Duration::from_millis(1)).await; // The other gets it almost immediately too. let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired7); + assert_eq!(acquired7, Some(3)); // new lock generation! sleep(Duration::from_millis(1)).await; - // But when we take a longer lease... + // But when we take a longer lease… let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired8); + assert_eq!(acquired8, Some(4)); // new lock generation! // It blocks the other user. let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(!acquired9); + assert!(acquired9.is_none()); // not acquired // We can hold onto our lease. let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired10); + assert_eq!(acquired10, Some(4)); // same lock generation } } }; diff --git a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs index 1ca2446c37b..73418ca7b78 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/memory_store.rs @@ -19,13 +19,16 @@ use std::{ use async_trait::async_trait; use matrix_sdk_common::{ - cross_process_lock::memory_store_helper::try_take_leased_lock, + cross_process_lock::{ + CrossProcessLockGeneration, + memory_store_helper::{Lease, try_take_leased_lock}, + }, linked_chunk::{ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position, RawChunk, Update, relational::RelationalLinkedChunk, }, }; -use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType, time::Instant}; +use ruma::{EventId, OwnedEventId, RoomId, events::relation::RelationType}; use tracing::error; use super::{EventCacheStore, EventCacheStoreError, Result, extract_event_relation}; @@ -41,7 +44,7 @@ pub struct MemoryStore { #[derive(Debug)] struct MemoryStoreInner { - leases: HashMap, + leases: HashMap, events: RelationalLinkedChunk, } @@ -73,7 +76,7 @@ impl EventCacheStore for MemoryStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { let mut inner = self.inner.write().unwrap(); Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder)) diff --git a/crates/matrix-sdk-base/src/event_cache/store/mod.rs b/crates/matrix-sdk-base/src/event_cache/store/mod.rs index 02ad91c04b9..7735b644f35 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/mod.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/mod.rs @@ -28,7 +28,8 @@ mod memory_store; mod traits; use matrix_sdk_common::cross_process_lock::{ - CrossProcessLock, CrossProcessLockError, CrossProcessLockGuard, TryLock, + CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard, + TryLock, }; pub use matrix_sdk_store_encryption::Error as StoreEncryptionError; use ruma::{OwnedEventId, events::AnySyncTimelineEvent, serde::Raw}; @@ -188,7 +189,7 @@ impl TryLock for LockableEventCacheStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> std::result::Result { + ) -> std::result::Result, Self::LockError> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await } } diff --git a/crates/matrix-sdk-base/src/event_cache/store/traits.rs b/crates/matrix-sdk-base/src/event_cache/store/traits.rs index aca4a8e6da8..2086aefd07c 100644 --- a/crates/matrix-sdk-base/src/event_cache/store/traits.rs +++ b/crates/matrix-sdk-base/src/event_cache/store/traits.rs @@ -17,6 +17,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; use matrix_sdk_common::{ AsyncTraitDeps, + cross_process_lock::CrossProcessLockGeneration, linked_chunk::{ ChunkIdentifier, ChunkIdentifierGenerator, ChunkMetadata, LinkedChunkId, Position, RawChunk, Update, @@ -46,7 +47,7 @@ pub trait EventCacheStore: AsyncTraitDeps { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result; + ) -> Result, Self::Error>; /// An [`Update`] reflects an operation that has happened inside a linked /// chunk. The linked chunk is used by the event cache to store the events @@ -196,7 +197,7 @@ impl EventCacheStore for EraseEventCacheStoreError { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-base/src/media/store/integration_tests.rs b/crates/matrix-sdk-base/src/media/store/integration_tests.rs index f7bbe59efb2..5dda5f12781 100644 --- a/crates/matrix-sdk-base/src/media/store/integration_tests.rs +++ b/crates/matrix-sdk-base/src/media/store/integration_tests.rs @@ -1337,57 +1337,57 @@ macro_rules! media_store_integration_tests_time { let store = get_media_store().await.unwrap(); let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired0); + assert_eq!(acquired0, Some(1)); // first lock generation // Should extend the lease automatically (same holder). let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired2); + assert_eq!(acquired2, Some(1)); // same lock generation // Should extend the lease automatically (same holder + time is ok). let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired3); + assert_eq!(acquired3, Some(1)); // same lock generation // Another attempt at taking the lock should fail, because it's taken. let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired4); + assert!(acquired4.is_none()); // not acquired // Even if we insist. let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired5); + assert!(acquired5.is_none()); // not acquired // That's a nice test we got here, go take a little nap. sleep(Duration::from_millis(50)).await; // Still too early. let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired55); + assert!(acquired55.is_none()); // not acquired // Ok you can take another nap then. sleep(Duration::from_millis(250)).await; // At some point, we do get the lock. let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap(); - assert!(acquired6); + assert_eq!(acquired6, Some(2)); // new lock generation! sleep(Duration::from_millis(1)).await; // The other gets it almost immediately too. let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired7); + assert_eq!(acquired7, Some(3)); // new lock generation! sleep(Duration::from_millis(1)).await; - // But when we take a longer lease... + // But when we take a longer lease… let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired8); + assert_eq!(acquired8, Some(4)); // new lock generation! // It blocks the other user. let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(!acquired9); + assert!(acquired9.is_none()); // not acquired // We can hold onto our lease. let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired10); + assert_eq!(acquired10, Some(4)); // same lock generation } } }; diff --git a/crates/matrix-sdk-base/src/media/store/memory_store.rs b/crates/matrix-sdk-base/src/media/store/memory_store.rs index 8b0fab3e01d..b97c343adcb 100644 --- a/crates/matrix-sdk-base/src/media/store/memory_store.rs +++ b/crates/matrix-sdk-base/src/media/store/memory_store.rs @@ -20,12 +20,13 @@ use std::{ use async_trait::async_trait; use matrix_sdk_common::{ - cross_process_lock::memory_store_helper::try_take_leased_lock, ring_buffer::RingBuffer, -}; -use ruma::{ - MxcUri, OwnedMxcUri, - time::{Instant, SystemTime}, + cross_process_lock::{ + CrossProcessLockGeneration, + memory_store_helper::{Lease, try_take_leased_lock}, + }, + ring_buffer::RingBuffer, }; +use ruma::{MxcUri, OwnedMxcUri, time::SystemTime}; use super::Result; use crate::media::{ @@ -48,7 +49,7 @@ pub struct MemoryMediaStore { #[derive(Debug)] struct MemoryMediaStoreInner { media: RingBuffer, - leases: HashMap, + leases: HashMap, media_retention_policy: Option, last_media_cleanup_time: SystemTime, } @@ -110,7 +111,7 @@ impl MediaStore for MemoryMediaStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { let mut inner = self.inner.write().unwrap(); Ok(try_take_leased_lock(&mut inner.leases, lease_duration_ms, key, holder)) diff --git a/crates/matrix-sdk-base/src/media/store/mod.rs b/crates/matrix-sdk-base/src/media/store/mod.rs index ce64a972620..5269c550c5f 100644 --- a/crates/matrix-sdk-base/src/media/store/mod.rs +++ b/crates/matrix-sdk-base/src/media/store/mod.rs @@ -32,7 +32,8 @@ use std::fmt; use std::{ops::Deref, sync::Arc}; use matrix_sdk_common::cross_process_lock::{ - CrossProcessLock, CrossProcessLockError, CrossProcessLockGuard, TryLock, + CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard, + TryLock, }; use matrix_sdk_store_encryption::Error as StoreEncryptionError; pub use traits::{DynMediaStore, IntoMediaStore, MediaStore, MediaStoreInner}; @@ -172,7 +173,7 @@ impl TryLock for LockableMediaStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> std::result::Result { + ) -> std::result::Result, Self::LockError> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await } } diff --git a/crates/matrix-sdk-base/src/media/store/traits.rs b/crates/matrix-sdk-base/src/media/store/traits.rs index 553ffa3d0ff..73b85e391e2 100644 --- a/crates/matrix-sdk-base/src/media/store/traits.rs +++ b/crates/matrix-sdk-base/src/media/store/traits.rs @@ -17,7 +17,7 @@ use std::{fmt, sync::Arc}; use async_trait::async_trait; -use matrix_sdk_common::AsyncTraitDeps; +use matrix_sdk_common::{AsyncTraitDeps, cross_process_lock::CrossProcessLockGeneration}; use ruma::{MxcUri, time::SystemTime}; #[cfg(doc)] @@ -41,7 +41,7 @@ pub trait MediaStore: AsyncTraitDeps { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result; + ) -> Result, Self::Error>; /// Add a media file's content in the media store. /// @@ -313,7 +313,7 @@ impl MediaStore for EraseMediaStoreError { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-common/src/cross_process_lock.rs b/crates/matrix-sdk-common/src/cross_process_lock.rs index 977ba62f8f8..80b832a7091 100644 --- a/crates/matrix-sdk-common/src/cross_process_lock.rs +++ b/crates/matrix-sdk-common/src/cross_process_lock.rs @@ -17,7 +17,7 @@ //! This is a per-process lock that may be used only for very specific use //! cases, where multiple processes might concurrently write to the same //! database at the same time; this would invalidate store caches, so -//! that should be done mindfully. Such a lock can be acquired multiple times by +//! that should be done mindfully. Such a lock can be obtained multiple times by //! the same process, and it remains active as long as there's at least one user //! in a given process. //! @@ -26,7 +26,7 @@ //! timestamp on the side; see also `CryptoStore::try_take_leased_lock` for more //! details. //! -//! The lock is initially acquired for a certain period of time (namely, the +//! The lock is initially obtainedd for a certain period of time (namely, the //! duration of a lease, aka `LEASE_DURATION_MS`), and then a “heartbeat” task //! renews the lease to extend its duration, every so often (namely, every //! `EXTEND_LEASE_EVERY_MS`). Since the Tokio scheduler might be busy, the @@ -42,13 +42,13 @@ use std::{ future::Future, sync::{ Arc, - atomic::{self, AtomicU32}, + atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}, }, time::Duration, }; use tokio::sync::Mutex; -use tracing::{debug, error, instrument, trace}; +use tracing::{debug, error, instrument, trace, warn}; use crate::{ SendOutsideWasm, @@ -56,6 +56,12 @@ use crate::{ sleep::sleep, }; +/// A lock generation is an integer incremented each time the lock is taken by +/// a different holder. +/// +/// This is used to know if a lock has been dirtied. +pub type CrossProcessLockGeneration = u64; + /// Trait used to try to take a lock. Foundation of [`CrossProcessLock`]. pub trait TryLock { #[cfg(not(target_family = "wasm"))] @@ -69,18 +75,23 @@ pub trait TryLock { /// This attempts to take a lock for the given lease duration. /// /// - If we already had the lease, this will extend the lease. - /// - If we didn't, but the previous lease has expired, we will acquire the + /// - If we didn't, but the previous lease has expired, we will obtain the /// lock. - /// - If there was no previous lease, we will acquire the lock. + /// - If there was no previous lease, we will obtain the lock. /// - Otherwise, we don't get the lock. /// - /// Returns whether taking the lock succeeded. + /// Returns `Some(_)` to indicate the lock succeeded, `None` otherwise. The + /// cross-process lock generation must be compared to the generation before + /// the call to see if the lock has been dirtied: a different generation + /// means the lock has been dirtied, i.e. taken by a different holder in + /// the meantime. fn try_lock( &self, lease_duration_ms: u32, key: &str, holder: &str, - ) -> impl Future> + SendOutsideWasm; + ) -> impl Future, Self::LockError>> + + SendOutsideWasm; } /// Small state machine to handle wait times. @@ -101,13 +112,19 @@ pub struct CrossProcessLockGuard { num_holders: Arc, } +impl CrossProcessLockGuard { + fn new(num_holders: Arc) -> Self { + Self { num_holders } + } +} + impl Drop for CrossProcessLockGuard { fn drop(&mut self) { - self.num_holders.fetch_sub(1, atomic::Ordering::SeqCst); + self.num_holders.fetch_sub(1, Ordering::SeqCst); } } -/// A store-based lock for a `Store`. +/// A cross-process lock implementation. /// /// See the doc-comment of this module for more information. #[derive(Clone, Debug)] @@ -124,7 +141,7 @@ where /// Number of holders of the lock in this process. /// - /// If greater than 0, this means we've already acquired this lock, in this + /// If greater than 0, this means we've already obtained this lock, in this /// process, and the store lock mustn't be touched. /// /// When the number of holders is decreased to 0, then the lock must be @@ -146,6 +163,15 @@ where /// Backoff time, in milliseconds. backoff: Arc>, + + /// This lock generation. + generation: Arc, + + /// Whether the lock has been dirtied. + /// + /// See [`CrossProcessLockResult::Dirty`] to learn more about the semantics + /// of _dirty_. + is_dirty: Arc, } /// Amount of time a lease of the lock should last, in milliseconds. @@ -166,6 +192,19 @@ const INITIAL_BACKOFF_MS: u32 = 10; /// we'll wait for the lock, *between two attempts*. pub const MAX_BACKOFF_MS: u32 = 1000; +/// Sentinel value representing the absence of a lock generation value. +/// +/// When the lock is created, it has no generation. Once locked, it receives its +/// first generation from [`TryLock::try_lock`]. Subsequent lockings may +/// generate new lock generation. The generation is incremented by 1 every time. +/// +/// The first generation is defined by [`FIRST_CROSS_PROCESS_LOCK_GENERATION`]. +pub const NO_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 0; + +/// Describe the first lock generation value (see +/// [`CrossProcessLockGeneration`]). +pub const FIRST_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 1; + impl CrossProcessLock where L: TryLock + Clone + SendOutsideWasm + 'static, @@ -185,45 +224,93 @@ where num_holders: Arc::new(0.into()), locking_attempt: Arc::new(Mutex::new(())), renew_task: Default::default(), + generation: Arc::new(AtomicU64::new(NO_CROSS_PROCESS_LOCK_GENERATION)), + is_dirty: Arc::new(AtomicBool::new(false)), } } + /// Determine whether the cross-process lock is dirty. + /// + /// See [`CrossProcessLockResult::Dirty`] to learn more about the semantics + /// of _dirty_. + pub fn is_dirty(&self) -> bool { + self.is_dirty.load(Ordering::SeqCst) + } + + /// Clear the dirty state from this cross-process lock. + /// + /// If the cross-process lock is dirtied, it will remain dirtied until + /// this method is called. This allows recovering from a dirty state and + /// marking that it has recovered. + pub fn clear_dirty(&self) { + self.is_dirty.store(false, Ordering::SeqCst); + self.generation.store(NO_CROSS_PROCESS_LOCK_GENERATION, Ordering::SeqCst); + } + /// Try to lock once, returns whether the lock was obtained or not. + /// + /// The lock can be obtained but it can be dirty. In all cases, the renew + /// task will run in the background. #[instrument(skip(self), fields(?self.lock_key, ?self.lock_holder))] - pub async fn try_lock_once( - &self, - ) -> Result, CrossProcessLockError> { + pub async fn try_lock_once(&self) -> Result { // Hold onto the locking attempt mutex for the entire lifetime of this // function, to avoid multiple reentrant calls. let mut _attempt = self.locking_attempt.lock().await; // If another thread obtained the lock, make sure to only superficially increase // the number of holders, and carry on. - if self.num_holders.load(atomic::Ordering::SeqCst) > 0 { + if self.num_holders.load(Ordering::SeqCst) > 0 { // Note: between the above load and the fetch_add below, another thread may // decrement `num_holders`. That's fine because that means the lock // was taken by at least one thread, and after this call it will be // taken by at least one thread. trace!("We already had the lock, incrementing holder count"); - self.num_holders.fetch_add(1, atomic::Ordering::SeqCst); - let guard = CrossProcessLockGuard { num_holders: self.num_holders.clone() }; - return Ok(Some(guard)); + + self.num_holders.fetch_add(1, Ordering::SeqCst); + + return Ok(CrossProcessLockResult::Clean(CrossProcessLockGuard::new( + self.num_holders.clone(), + ))); } - let acquired = self + if let Some(new_generation) = self .locker .try_lock(LEASE_DURATION_MS, &self.lock_key, &self.lock_holder) .await - .map_err(|err| CrossProcessLockError::TryLockError(Box::new(err)))?; + .map_err(|err| CrossProcessLockError::TryLockError(Box::new(err)))? + { + match self.generation.swap(new_generation, Ordering::SeqCst) { + // If there was no lock generation, it means this is the first time the lock is + // obtained. It cannot be dirty. + NO_CROSS_PROCESS_LOCK_GENERATION => { + trace!(?new_generation, "Setting the lock generation for the first time"); + } + + // This was NOT the same generation, the lock has been dirtied! + previous_generation if previous_generation != new_generation => { + warn!( + ?previous_generation, + ?new_generation, + "The lock has been obtained, but it's been dirtied!" + ); + self.is_dirty.store(true, Ordering::SeqCst); + } - if !acquired { - trace!("Couldn't acquire the lock immediately."); - return Ok(None); + // This was the same generation, no problem. + _ => { + trace!("Same lock generation; no problem"); + } + } + + trace!("Lock obtained!"); + } else { + trace!("Couldn't obtain the lock immediately."); + return Ok(CrossProcessLockResult::Unobtained); } - trace!("Acquired the lock, spawning the lease extension task."); + trace!("Obtained the lock, spawning the lease extension task."); - // This is the first time we've acquired the lock. We're going to spawn the task + // This is the first time we've obtaind the lock. We're going to spawn the task // that will renew the lease. // Clone data to be owned by the task. @@ -260,7 +347,7 @@ where let _guard = this.locking_attempt.lock().await; // If there are no more users, we can quit. - if this.num_holders.load(atomic::Ordering::SeqCst) == 0 { + if this.num_holders.load(Ordering::SeqCst) == 0 { trace!("exiting the lease extension loop"); // Cancel the lease with another 0ms lease. @@ -275,21 +362,48 @@ where sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await; - let fut = - this.locker.try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder); + match this + .locker + .try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder) + .await + { + Ok(Some(_generation)) => { + // It's impossible that the generation can be different + // from the previous generation. + // + // As long as the task runs, the lock is renewed, so the + // generation remains the same. If the lock is not + // taken, it's because the lease has expired, which is + // represented by the `Ok(None)` value, and the task + // must stop. + } + + Ok(None) => { + error!("Failed to renew the lock lease: the lock could not be obtained"); + + // Exit the loop. + break; + } + + Err(err) => { + error!("Error when extending the lock lease: {err:#}"); - if let Err(err) = fut.await { - error!("error when extending lock lease: {err:#}"); - // Exit the loop. - break; + // Exit the loop. + break; + } } } })); - self.num_holders.fetch_add(1, atomic::Ordering::SeqCst); + self.num_holders.fetch_add(1, Ordering::SeqCst); - let guard = CrossProcessLockGuard { num_holders: self.num_holders.clone() }; - Ok(Some(guard)) + let guard = CrossProcessLockGuard::new(self.num_holders.clone()); + + Ok(if self.is_dirty() { + CrossProcessLockResult::Dirty(guard) + } else { + CrossProcessLockResult::Clean(guard) + }) } /// Attempt to take the lock, with exponential backoff if the lock has @@ -311,7 +425,7 @@ where // lock in `try_lock_once` should sequentialize it all. loop { - if let Some(guard) = self.try_lock_once().await? { + if let Some(guard) = self.try_lock_once().await?.ok() { // Reset backoff before returning, for the next attempt to lock. *self.backoff.lock().await = WaitingTime::Some(INITIAL_BACKOFF_MS); return Ok(guard); @@ -348,6 +462,42 @@ where } } +/// Represent the result of a locking attempt, either by +/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`]. +#[derive(Debug)] +pub enum CrossProcessLockResult { + /// The lock has been obtained successfully, all good. + Clean(CrossProcessLockGuard), + + /// The lock has been obtained successfully, but the lock is dirty! + /// + /// This holder has obtained this cross-process lock once, then another + /// holder has obtained this cross-process lock _before_ this holder + /// obtained it again. The lock is marked as dirty. It means the value + /// protected by the cross-process lock may need to be reloaded if + /// synchronisation is important. + Dirty(CrossProcessLockGuard), + + /// The lock has not been obtained. + Unobtained, +} + +impl CrossProcessLockResult { + /// Convert from [`CrossProcessLockResult`] to + /// [`Option`] where `T` is [`CrossProcessLockGuard`]. + pub fn ok(self) -> Option { + match self { + Self::Clean(guard) | Self::Dirty(guard) => Some(guard), + Self::Unobtained => None, + } + } + + /// Return `true` if the lock has been obtained, `false` otherwise. + pub fn is_ok(&self) -> bool { + matches!(self, Self::Clean(_) | Self::Dirty(_)) + } +} + /// Error related to the locking API of the store. #[derive(Debug, thiserror::Error)] pub enum CrossProcessLockError { @@ -369,8 +519,8 @@ pub enum CrossProcessLockError { mod tests { use std::{ collections::HashMap, + ops::Not, sync::{Arc, RwLock, atomic}, - time::Instant, }; use assert_matches::assert_matches; @@ -381,17 +531,23 @@ mod tests { }; use super::{ - CrossProcessLock, CrossProcessLockError, CrossProcessLockGuard, EXTEND_LEASE_EVERY_MS, - TryLock, memory_store_helper::try_take_leased_lock, + CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, + CrossProcessLockResult, EXTEND_LEASE_EVERY_MS, TryLock, + memory_store_helper::{Lease, try_take_leased_lock}, }; #[derive(Clone, Default)] struct TestStore { - leases: Arc>>, + leases: Arc>>, } impl TestStore { - fn try_take_leased_lock(&self, lease_duration_ms: u32, key: &str, holder: &str) -> bool { + fn try_take_leased_lock( + &self, + lease_duration_ms: u32, + key: &str, + holder: &str, + ) -> Option { try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder) } } @@ -408,13 +564,13 @@ mod tests { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::LockError> { Ok(self.try_take_leased_lock(lease_duration_ms, key, holder)) } } - async fn release_lock(guard: Option) { - drop(guard); + async fn release_lock(result: CrossProcessLockResult) { + drop(result); sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await; } @@ -426,19 +582,19 @@ mod tests { let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned()); // The lock plain works when used with a single holder. - let acquired = lock.try_lock_once().await?; - assert!(acquired.is_some()); + let result = lock.try_lock_once().await?; + assert!(result.is_ok()); assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1); // Releasing works. - release_lock(acquired).await; + release_lock(result).await; assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0); // Spin locking on the same lock always works, assuming no concurrent access. - let acquired = lock.spin_lock(None).await.unwrap(); + let guard = lock.spin_lock(None).await.unwrap(); // Releasing still works. - release_lock(Some(acquired)).await; + release_lock(CrossProcessLockResult::Clean(guard)).await; assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0); Ok(()) @@ -449,9 +605,9 @@ mod tests { let store = TestStore::default(); let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned()); - // When a lock is acquired... - let acquired = lock.try_lock_once().await?; - assert!(acquired.is_some()); + // When a lock is obtained... + let result = lock.try_lock_once().await?; + assert!(result.is_ok()); assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1); // But then forgotten... (note: no need to release the guard) @@ -461,8 +617,8 @@ mod tests { let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned()); // We still got it. - let acquired = lock.try_lock_once().await?; - assert!(acquired.is_some()); + let result = lock.try_lock_once().await?; + assert!(result.is_ok()); assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1); Ok(()) @@ -474,19 +630,19 @@ mod tests { let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned()); // Taking the lock twice... - let acquired = lock.try_lock_once().await?; - assert!(acquired.is_some()); + let result1 = lock.try_lock_once().await?; + assert!(result1.is_ok()); - let acquired2 = lock.try_lock_once().await?; - assert!(acquired2.is_some()); + let result2 = lock.try_lock_once().await?; + assert!(result2.is_ok()); assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 2); // ...means we can release it twice. - release_lock(acquired).await; + release_lock(result1).await; assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1); - release_lock(acquired2).await; + release_lock(result2).await; assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0); Ok(()) @@ -499,22 +655,22 @@ mod tests { let lock2 = CrossProcessLock::new(store, "key".to_owned(), "second".to_owned()); // When the first process takes the lock... - let acquired1 = lock1.try_lock_once().await?; - assert!(acquired1.is_some()); + let result1 = lock1.try_lock_once().await?; + assert!(result1.is_ok()); // The second can't take it immediately. - let acquired2 = lock2.try_lock_once().await?; - assert!(acquired2.is_none()); + let result2 = lock2.try_lock_once().await?; + assert!(result2.is_ok().not()); let lock2_clone = lock2.clone(); let handle = spawn(async move { lock2_clone.spin_lock(Some(1000)).await }); sleep(Duration::from_millis(100)).await; - drop(acquired1); + drop(result1); // lock2 in the background manages to get the lock at some point. - let _acquired2 = handle + let _result2 = handle .await .expect("join handle is properly awaited") .expect("lock was obtained after spin-locking"); @@ -533,48 +689,63 @@ pub mod memory_store_helper { use ruma::time::{Duration, Instant}; + use super::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION}; + + #[derive(Debug)] + pub struct Lease { + holder: String, + expiration: Instant, + generation: CrossProcessLockGeneration, + } + pub fn try_take_leased_lock( - leases: &mut HashMap, + leases: &mut HashMap, lease_duration_ms: u32, key: &str, holder: &str, - ) -> bool { + ) -> Option { let now = Instant::now(); let expiration = now + Duration::from_millis(lease_duration_ms.into()); match leases.entry(key.to_owned()) { // There is an existing holder. Entry::Occupied(mut entry) => { - let (current_holder, current_expiration) = entry.get_mut(); + let Lease { + holder: current_holder, + expiration: current_expiration, + generation: current_generation, + } = entry.get_mut(); if current_holder == holder { // We had the lease before, extend it. *current_expiration = expiration; - true + Some(*current_generation) } else { // We didn't have it. if *current_expiration < now { // Steal it! *current_holder = holder.to_owned(); *current_expiration = expiration; + *current_generation += 1; - true + Some(*current_generation) } else { // We tried our best. - false + None } } } // There is no holder, easy. Entry::Vacant(entry) => { - entry.insert(( - holder.to_owned(), - Instant::now() + Duration::from_millis(lease_duration_ms.into()), - )); + entry.insert(Lease { + holder: holder.to_owned(), + expiration: Instant::now() + Duration::from_millis(lease_duration_ms.into()), + generation: FIRST_CROSS_PROCESS_LOCK_GENERATION, + }); - true + Some(FIRST_CROSS_PROCESS_LOCK_GENERATION) } } } diff --git a/crates/matrix-sdk-crypto/src/store/integration_tests.rs b/crates/matrix-sdk-crypto/src/store/integration_tests.rs index 39314c61a79..7e818f81e52 100644 --- a/crates/matrix-sdk-crypto/src/store/integration_tests.rs +++ b/crates/matrix-sdk-crypto/src/store/integration_tests.rs @@ -1457,57 +1457,57 @@ macro_rules! cryptostore_integration_tests_time { let (_account, store) = get_loaded_store("lease_locks").await; let acquired0 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired0); + assert_eq!(acquired0, Some(1)); // first generation // Should extend the lease automatically (same holder). let acquired2 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired2); + assert_eq!(acquired2, Some(1)); // same lock generation // Should extend the lease automatically (same holder + time is ok). let acquired3 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(acquired3); + assert_eq!(acquired3, Some(1)); // same lock generation // Another attempt at taking the lock should fail, because it's taken. let acquired4 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired4); + assert!(acquired4.is_none()); // not acquired // Even if we insist. let acquired5 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired5); + assert!(acquired5.is_none()); // That's a nice test we got here, go take a little nap. tokio::time::sleep(Duration::from_millis(50)).await; // Still too early. let acquired55 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(!acquired55); + assert!(acquired55.is_none()); // not acquired // Ok you can take another nap then. tokio::time::sleep(Duration::from_millis(250)).await; // At some point, we do get the lock. let acquired6 = store.try_take_leased_lock(0, "key", "bob").await.unwrap(); - assert!(acquired6); + assert_eq!(acquired6, Some(2)); // new lock generation! tokio::time::sleep(Duration::from_millis(1)).await; // The other gets it almost immediately too. let acquired7 = store.try_take_leased_lock(0, "key", "alice").await.unwrap(); - assert!(acquired7); + assert_eq!(acquired7, Some(3)); // new lock generation! tokio::time::sleep(Duration::from_millis(1)).await; - // But when we take a longer lease... + // But when we take a longer lease… let acquired8 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired8); + assert_eq!(acquired8, Some(4)); // new lock generation! // It blocks the other user. let acquired9 = store.try_take_leased_lock(300, "key", "alice").await.unwrap(); - assert!(!acquired9); + assert!(acquired9.is_none()); // not acquired // We can hold onto our lease. let acquired10 = store.try_take_leased_lock(300, "key", "bob").await.unwrap(); - assert!(acquired10); + assert_eq!(acquired10, Some(4)); // same lock generation } } }; diff --git a/crates/matrix-sdk-crypto/src/store/memorystore.rs b/crates/matrix-sdk-crypto/src/store/memorystore.rs index c1f12bc2c15..9b659801d67 100644 --- a/crates/matrix-sdk-crypto/src/store/memorystore.rs +++ b/crates/matrix-sdk-crypto/src/store/memorystore.rs @@ -20,11 +20,15 @@ use std::{ use async_trait::async_trait; use matrix_sdk_common::{ - cross_process_lock::memory_store_helper::try_take_leased_lock, locks::RwLock as StdRwLock, + cross_process_lock::{ + memory_store_helper::{try_take_leased_lock, Lease}, + CrossProcessLockGeneration, + }, + locks::RwLock as StdRwLock, }; use ruma::{ - events::secret::request::SecretName, time::Instant, DeviceId, OwnedDeviceId, OwnedRoomId, - OwnedTransactionId, OwnedUserId, RoomId, TransactionId, UserId, + events::secret::request::SecretName, DeviceId, OwnedDeviceId, OwnedRoomId, OwnedTransactionId, + OwnedUserId, RoomId, TransactionId, UserId, }; use tokio::sync::{Mutex, RwLock}; use tracing::warn; @@ -99,7 +103,7 @@ pub struct MemoryStore { key_requests_by_info: StdRwLock>, direct_withheld_info: StdRwLock>>, custom_values: StdRwLock>>, - leases: StdRwLock>, + leases: StdRwLock>, secret_inbox: StdRwLock>>, backup_keys: RwLock, dehydrated_device_pickle_key: RwLock>, @@ -768,7 +772,7 @@ impl CryptoStore for MemoryStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result> { Ok(try_take_leased_lock(&mut self.leases.write(), lease_duration_ms, key, holder)) } } @@ -1269,6 +1273,7 @@ mod integration_tests { }; use async_trait::async_trait; + use matrix_sdk_common::cross_process_lock::CrossProcessLockGeneration; use ruma::{ events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId, }; @@ -1586,7 +1591,7 @@ mod integration_tests { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await } diff --git a/crates/matrix-sdk-crypto/src/store/mod.rs b/crates/matrix-sdk-crypto/src/store/mod.rs index f4004e08ff1..68b0cbd4dec 100644 --- a/crates/matrix-sdk-crypto/src/store/mod.rs +++ b/crates/matrix-sdk-crypto/src/store/mod.rs @@ -100,7 +100,9 @@ pub mod integration_tests; pub(crate) use crypto_store_wrapper::CryptoStoreWrapper; pub use error::{CryptoStoreError, Result}; use matrix_sdk_common::{ - cross_process_lock::CrossProcessLock, deserialized_responses::WithheldCode, timeout::timeout, + cross_process_lock::{CrossProcessLock, CrossProcessLockGeneration}, + deserialized_responses::WithheldCode, + timeout::timeout, }; pub use memorystore::MemoryStore; pub use traits::{CryptoStore, DynCryptoStore, IntoCryptoStore}; @@ -1790,7 +1792,7 @@ impl matrix_sdk_common::cross_process_lock::TryLock for LockableCryptoStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> std::result::Result { + ) -> std::result::Result, Self::LockError> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await } } diff --git a/crates/matrix-sdk-crypto/src/store/traits.rs b/crates/matrix-sdk-crypto/src/store/traits.rs index 9643ab4c06a..eaf2fb9be25 100644 --- a/crates/matrix-sdk-crypto/src/store/traits.rs +++ b/crates/matrix-sdk-crypto/src/store/traits.rs @@ -15,7 +15,7 @@ use std::{collections::HashMap, fmt, sync::Arc}; use async_trait::async_trait; -use matrix_sdk_common::AsyncTraitDeps; +use matrix_sdk_common::{cross_process_lock::CrossProcessLockGeneration, AsyncTraitDeps}; use ruma::{ events::secret::request::SecretName, DeviceId, OwnedDeviceId, RoomId, TransactionId, UserId, }; @@ -395,7 +395,7 @@ pub trait CryptoStore: AsyncTraitDeps { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result; + ) -> Result, Self::Error>; /// Load the next-batch token for a to-device query, if any. async fn next_batch_token(&self) -> Result, Self::Error>; @@ -641,7 +641,7 @@ impl CryptoStore for EraseCryptoStoreError { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, Self::Error> { self.0.try_take_leased_lock(lease_duration_ms, key, holder).await.map_err(Into::into) } diff --git a/crates/matrix-sdk-indexeddb/Cargo.toml b/crates/matrix-sdk-indexeddb/Cargo.toml index 51aef8e7003..08fa1b59c2e 100644 --- a/crates/matrix-sdk-indexeddb/Cargo.toml +++ b/crates/matrix-sdk-indexeddb/Cargo.toml @@ -18,7 +18,7 @@ default = ["e2e-encryption", "state-store", "event-cache-store"] event-cache-store = ["dep:matrix-sdk-base", "media-store"] media-store = ["dep:matrix-sdk-base"] state-store = ["dep:matrix-sdk-base", "growable-bloom-filter"] -e2e-encryption = ["dep:matrix-sdk-crypto"] +e2e-encryption = ["dep:matrix-sdk-base", "dep:matrix-sdk-crypto"] testing = ["matrix-sdk-crypto?/testing"] experimental-encrypted-state-events = [ "matrix-sdk-crypto?/experimental-encrypted-state-events" diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index 7b95dba018b..ae862435348 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -29,6 +29,7 @@ use crate::{crypto_store::Result, serializer::SafeEncodeSerializer, IndexeddbCry mod old_keys; mod v0_to_v5; +mod v101_to_v102; mod v10_to_v11; mod v11_to_v12; mod v12_to_v13; @@ -186,6 +187,10 @@ pub async fn open_and_upgrade_db( v14_to_v101::schema_delete(name).await?; } + if old_version < 102 { + v101_to_v102::schema_add(name).await?; + } + // If you add more migrations here, you'll need to update // `tests::EXPECTED_SCHEMA_VERSION`. @@ -290,7 +295,7 @@ mod tests { wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); /// The schema version we expect after we open the store. - const EXPECTED_SCHEMA_VERSION: u32 = 101; + const EXPECTED_SCHEMA_VERSION: u32 = 102; /// Adjust this to test do a more comprehensive perf test const NUM_RECORDS_FOR_PERF: usize = 2_000; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs new file mode 100644 index 00000000000..9ee0d9be09e --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v101_to_v102.rs @@ -0,0 +1,34 @@ +// Copyright 2025 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use indexed_db_futures::{error::OpenDbError, Build}; + +use crate::crypto_store::{keys, migrations::do_schema_upgrade, Result}; + +/// Perform the schema upgrade v101 to v102, add the `lease_locks` table. +/// +/// Note: it's not trivial to delete old lease locks in the `keys::CORE` table, +/// because we don't know which keys were associated to them. We consider it's +/// fine because it represents a tiny amount of data (maybe 2 rows, with the +/// “`Lease` type” being quite small). To achieve so, we could read all rows in +/// `keys::CORE`, try to deserialize to the `Lease` type, and act accordingly, +/// but each store may have its own `Lease` type. Also note that this +/// `matrix-sdk-indexeddb` +pub(crate) async fn schema_add(name: &str) -> Result<(), OpenDbError> { + do_schema_upgrade(name, 102, |tx, _| { + tx.db().create_object_store(keys::LEASE_LOCKS).build()?; + Ok(()) + }) + .await +} diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs index 0b8215cecda..f35e591bb17 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/mod.rs @@ -30,6 +30,9 @@ use indexed_db_futures::{ KeyRange, }; use js_sys::Array; +use matrix_sdk_base::cross_process_lock::{ + CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, +}; use matrix_sdk_crypto::{ olm::{ Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession, @@ -51,6 +54,7 @@ use ruma::{ events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId, }; +use serde::{Deserialize, Serialize}; use sha2::Sha256; use tokio::sync::Mutex; use tracing::{debug, warn}; @@ -96,6 +100,8 @@ mod keys { pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles"; + pub const LEASE_LOCKS: &str = "lease_locks"; + // keys pub const STORE_CIPHER: &str = "store_cipher"; pub const ACCOUNT: &str = "account"; @@ -771,9 +777,9 @@ macro_rules! impl_crypto_store { impl_crypto_store! { async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> { - // Serialize calls to `save_pending_changes`; there are multiple await points below, and we're - // pickling data as we go, so we don't want to invalidate data we've previously read and - // overwrite it in the store. + // Serialize calls to `save_pending_changes`; there are multiple await points + // below, and we're pickling data as we go, so we don't want to + // invalidate data we've previously read and overwrite it in the store. // TODO: #2000 should make this lock go away, or change its shape. let _guard = self.save_changes_lock.lock().await; @@ -809,9 +815,9 @@ impl_crypto_store! { } async fn save_changes(&self, changes: Changes) -> Result<()> { - // Serialize calls to `save_changes`; there are multiple await points below, and we're - // pickling data as we go, so we don't want to invalidate data we've previously read and - // overwrite it in the store. + // Serialize calls to `save_changes`; there are multiple await points below, and + // we're pickling data as we go, so we don't want to invalidate data + // we've previously read and overwrite it in the store. // TODO: #2000 should make this lock go away, or change its shape. let _guard = self.save_changes_lock.lock().await; @@ -1100,9 +1106,7 @@ impl_crypto_store! { .collect(); let upper_bound: Array = [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect(); - let key = KeyRange::Bound( - lower_bound, true, - upper_bound, true); + let key = KeyRange::Bound(lower_bound, true, upper_bound, true); let tx = self .inner @@ -1161,8 +1165,8 @@ impl_crypto_store! { let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?; - // XXX ideally we would use `get_all_with_key_and_limit`, but that doesn't appear to be - // exposed (https://github.com/Alorel/rust-indexed-db/issues/31). Instead we replicate + // XXX ideally we would use `get_all_with_key_and_limit`, but that doesn't + // appear to be exposed (https://github.com/Alorel/rust-indexed-db/issues/31). Instead we replicate // the behaviour with a cursor. let Some(mut cursor) = idx.open_cursor().await? else { return Ok(vec![]); @@ -1331,7 +1335,9 @@ impl_crypto_store! { .with_mode(TransactionMode::Readonly) .build()? .object_store(keys::OLM_HASHES)? - .get::(&self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash))) + .get::( + &self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)), + ) .await? .is_some()) } @@ -1362,14 +1368,12 @@ impl_crypto_store! { async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> { let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str()); - let transaction = self.inner + let transaction = self + .inner .transaction(keys::SECRETS_INBOX) .with_mode(TransactionMode::Readwrite) .build()?; - transaction - .object_store(keys::SECRETS_INBOX)? - .delete(&range) - .build()?; + transaction.object_store(keys::SECRETS_INBOX)?.delete(&range).build()?; transaction.commit().await?; Ok(()) @@ -1383,7 +1387,9 @@ impl_crypto_store! { let val = self .inner - .transaction(keys::GOSSIP_REQUESTS).with_mode( TransactionMode::Readonly).build()? + .transaction(keys::GOSSIP_REQUESTS) + .with_mode(TransactionMode::Readonly) + .build()? .object_store(keys::GOSSIP_REQUESTS)? .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)? .get(key) @@ -1483,7 +1489,9 @@ impl_crypto_store! { let key = self.serializer.encode_key(keys::WITHHELD_SESSIONS, (room_id, session_id)); if let Some(pickle) = self .inner - .transaction(keys::WITHHELD_SESSIONS).with_mode(TransactionMode::Readonly).build()? + .transaction(keys::WITHHELD_SESSIONS) + .with_mode(TransactionMode::Readonly) + .build()? .object_store(keys::WITHHELD_SESSIONS)? .get(&key) .await? @@ -1561,10 +1569,8 @@ impl_crypto_store! { #[allow(clippy::unused_async)] // Mandated by trait on wasm. async fn set_custom_value(&self, key: &str, value: Vec) -> Result<()> { - let transaction = self.inner - .transaction(keys::CORE) - .with_mode(TransactionMode::Readwrite) - .build()?; + let transaction = + self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?; transaction .object_store(keys::CORE)? .put(&self.serializer.serialize_value(&value)?) @@ -1576,14 +1582,9 @@ impl_crypto_store! { #[allow(clippy::unused_async)] // Mandated by trait on wasm. async fn remove_custom_value(&self, key: &str) -> Result<()> { - let transaction = self.inner - .transaction(keys::CORE) - .with_mode(TransactionMode::Readwrite) - .build()?; - transaction - .object_store(keys::CORE)? - .delete(&JsValue::from_str(key)) - .build()?; + let transaction = + self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?; + transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(key)).build()?; transaction.commit().await?; Ok(()) } @@ -1593,53 +1594,68 @@ impl_crypto_store! { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result> { // As of 2023-06-23, the code below hasn't been tested yet. let key = JsValue::from_str(key); - let txn = - self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?; - let object_store = txn.object_store(keys::CORE)?; + let txn = self + .inner + .transaction(keys::LEASE_LOCKS) + .with_mode(TransactionMode::Readwrite) + .build()?; + let object_store = txn.object_store(keys::LEASE_LOCKS)?; - #[derive(serde::Deserialize, serde::Serialize)] + #[derive(Deserialize, Serialize)] struct Lease { holder: String, - expiration_ts: u64, + expiration: u64, + generation: CrossProcessLockGeneration, } - let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); - let expiration_ts = now_ts + lease_duration_ms as u64; - - let prev = object_store.get(&key).await?; - match prev { - Some(prev) => { - let lease: Lease = self.serializer.deserialize_value(prev)?; - if lease.holder == holder || lease.expiration_ts < now_ts { - object_store - .put( - &self.serializer.serialize_value(&Lease { - holder: holder.to_owned(), - expiration_ts, - })?, - ) - .with_key(key) - .build()?; - Ok(true) + let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); + let expiration = now + lease_duration_ms as u64; + + let lease = match object_store.get(&key).await? { + Some(entry) => { + let mut lease: Lease = self.serializer.deserialize_value(entry)?; + + if lease.holder == holder { + // We had the lease before, extend it. + lease.expiration = expiration; + + Some(lease) } else { - Ok(false) + // We didn't have it. + if lease.expiration < now { + // Steal it! + lease.holder = holder.to_owned(); + lease.expiration = expiration; + lease.generation += 1; + + Some(lease) + } else { + // We tried our best. + None + } } } None => { - object_store - .put( - &self - .serializer - .serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?, - ) - .with_key(key) - .build()?; - Ok(true) + let lease = Lease { + holder: holder.to_owned(), + expiration, + generation: FIRST_CROSS_PROCESS_LOCK_GENERATION, + }; + + Some(lease) } - } + }; + + Ok(if let Some(lease) = lease { + object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?; + + Some(lease.generation) + } else { + None + }) } } @@ -1851,7 +1867,7 @@ where } /// The objects we store in the gossip_requests indexeddb object store -#[derive(Debug, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Serialize, Deserialize)] struct GossipRequestIndexedDbObject { /// Encrypted hash of the [`SecretInfo`] structure. info: String, @@ -1876,7 +1892,7 @@ struct GossipRequestIndexedDbObject { } /// The objects we store in the inbound_group_sessions3 indexeddb object store -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Serialize, Deserialize)] struct InboundGroupSessionIndexedDbObject { /// Possibly encrypted /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`] diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs index dedd6426bed..c82a2447289 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/migrations.rs @@ -15,15 +15,16 @@ use indexed_db_futures::{ database::Database, error::{DomException, Error, OpenDbError}, + transaction::Transaction, }; use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v1, Version}; + use super::{v2, Version}; - pub const VERSION: Version = Version::V1; - pub use v1::keys; + pub const VERSION: Version = Version::V2; + pub use v2::keys; } /// Opens a connection to the IndexedDB database and takes care of upgrading it @@ -35,7 +36,7 @@ pub async fn open_and_upgrade_db(name: &str) -> Result { .with_on_upgrade_needed(|event, transaction| { let mut version = Version::try_from(event.old_version() as u32)?; while version < current::VERSION { - version = match version.upgrade(transaction.db())? { + version = match version.upgrade(transaction)? { Some(next) => next, None => current::VERSION, /* No more upgrades to apply, jump forward! */ }; @@ -49,18 +50,21 @@ pub async fn open_and_upgrade_db(name: &str) -> Result { #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(u32)] pub enum Version { - /// Version 0 of the database, for details see [`v0`] + /// Version 0 of the database, for details see [`v0`]. V0 = 0, - /// Version 1 of the database, for details see [`v1`] + /// Version 1 of the database, for details see [`v1`]. V1 = 1, + /// Version 2 of the database, for details see [`v2`]. + V2 = 2, } impl Version { /// Upgrade the database to the next version, if one exists. - pub fn upgrade(self, db: &Database) -> Result, Error> { + pub fn upgrade(self, transaction: &Transaction<'_>) -> Result, Error> { match self { - Self::V0 => v0::upgrade(db).map(Some), - Self::V1 => Ok(None), + Self::V0 => v0::upgrade(transaction).map(Some), + Self::V1 => v1::upgrade(transaction).map(Some), + Self::V2 => Ok(None), } } } @@ -76,6 +80,7 @@ impl TryFrom for Version { match value { 0 => Ok(Version::V0), 1 => Ok(Version::V1), + 2 => Ok(Version::V2), v => Err(UnknownVersionError(v)), } } @@ -96,8 +101,8 @@ pub mod v0 { use super::*; /// Upgrade database from `v0` to `v1` - pub fn upgrade(db: &Database) -> Result { - v1::create_object_stores(db)?; + pub fn upgrade(transaction: &Transaction<'_>) -> Result { + v1::create_object_stores(transaction.db())?; Ok(Version::V1) } } @@ -197,4 +202,27 @@ pub mod v1 { db.create_object_store(keys::GAPS).with_key_path(keys::GAPS_KEY_PATH.into()).build()?; Ok(()) } + + /// Upgrade database from `v1` to `v2` + pub fn upgrade(transaction: &Transaction<'_>) -> Result { + v2::empty_leases(transaction)?; + Ok(Version::V2) + } +} + +mod v2 { + // Re-use all the same keys from `v1`. + pub use super::v1::keys; + use super::*; + + /// The format of [`Lease`][super::super::types::Lease] is changing. Let's + /// erase previous values. + pub fn empty_leases(transaction: &Transaction<'_>) -> Result<(), Error> { + let object_store = transaction.object_store(keys::LEASES)?; + + // Remove all previous leases. + object_store.clear()?; + + Ok(()) + } } diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs index dc76ecc16ea..97a6274914c 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/mod.rs @@ -17,6 +17,10 @@ use std::{rc::Rc, time::Duration}; use indexed_db_futures::{database::Database, Build}; +#[cfg(target_family = "wasm")] +use matrix_sdk_base::cross_process_lock::{ + CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, +}; use matrix_sdk_base::{ event_cache::{store::EventCacheStore, Event, Gap}, linked_chunk::{ @@ -103,29 +107,57 @@ impl EventCacheStore for IndexeddbEventCacheStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, IndexeddbEventCacheStoreError> { let _timer = timer!("method"); - let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into()); - let transaction = self.transaction(&[Lease::OBJECT_STORE], IdbTransactionMode::Readwrite)?; - if let Some(lease) = transaction.get_lease_by_id(key).await? { - if lease.holder != holder && !lease.has_expired(now) { - return Ok(false); + let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into()); + let expiration = now + Duration::from_millis(lease_duration_ms.into()); + + let lease = match transaction.get_lease_by_id(key).await? { + Some(mut lease) => { + if lease.holder == holder { + // We had the lease before, extend it. + lease.expiration = expiration; + + Some(lease) + } else { + // We didn't have it. + if lease.expiration < now { + // Steal it! + lease.holder = holder.to_owned(); + lease.expiration = expiration; + lease.generation += 1; + + Some(lease) + } else { + // We tried our best. + None + } + } } - } + None => { + let lease = Lease { + key: key.to_owned(), + holder: holder.to_owned(), + expiration, + generation: FIRST_CROSS_PROCESS_LOCK_GENERATION, + }; + + Some(lease) + } + }; - transaction - .put_lease(&Lease { - key: key.to_owned(), - holder: holder.to_owned(), - expiration: now + Duration::from_millis(lease_duration_ms.into()), - }) - .await?; - transaction.commit().await?; - Ok(true) + Ok(if let Some(lease) = lease { + transaction.put_lease(&lease).await?; + transaction.commit().await?; + + Some(lease.generation) + } else { + None + }) } #[instrument(skip(self, updates))] diff --git a/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs b/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs index 317ca628fee..69afee898e3 100644 --- a/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs +++ b/crates/matrix-sdk-indexeddb/src/event_cache_store/types.rs @@ -15,6 +15,7 @@ use std::time::Duration; use matrix_sdk_base::{ + cross_process_lock::CrossProcessLockGeneration, deserialized_responses::TimelineEvent, event_cache::store::extract_event_relation, linked_chunk::{ChunkIdentifier, LinkedChunkId, OwnedLinkedChunkId}, @@ -29,13 +30,7 @@ pub struct Lease { pub key: String, pub holder: String, pub expiration: Duration, -} - -impl Lease { - /// Determines whether the lease is expired at a given time `t` - pub fn has_expired(&self, t: Duration) -> bool { - self.expiration < t - } + pub generation: CrossProcessLockGeneration, } /// Representation of a [`Chunk`](matrix_sdk_base::linked_chunk::Chunk) diff --git a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs index b866ea5071b..5d115498e0a 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/migrations.rs @@ -15,15 +15,16 @@ use indexed_db_futures::{ database::Database, error::{DomException, Error, OpenDbError}, + transaction::Transaction, }; use thiserror::Error; /// The current version and keys used in the database. pub mod current { - use super::{v1, Version}; + use super::{v2, Version}; - pub const VERSION: Version = Version::V1; - pub use v1::keys; + pub const VERSION: Version = Version::V2; + pub use v2::keys; } /// Opens a connection to the IndexedDB database and takes care of upgrading it @@ -35,7 +36,7 @@ pub async fn open_and_upgrade_db(name: &str) -> Result { .with_on_upgrade_needed(|event, transaction| { let mut version = Version::try_from(event.old_version() as u32)?; while version < current::VERSION { - version = match version.upgrade(transaction.db())? { + version = match version.upgrade(transaction)? { Some(next) => next, None => current::VERSION, /* No more upgrades to apply, jump forward! */ }; @@ -49,18 +50,21 @@ pub async fn open_and_upgrade_db(name: &str) -> Result { #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(u32)] pub enum Version { - /// Version 0 of the database, for details see [`v0`] + /// Version 0 of the database, for details see [`v0`]. V0 = 0, - /// Version 1 of the database, for details see [`v1`] + /// Version 1 of the database, for details see [`v1`]. V1 = 1, + /// Version 2 of the database, for details see [`v2`]. + V2 = 2, } impl Version { /// Upgrade the database to the next version, if one exists. - pub fn upgrade(self, db: &Database) -> Result, Error> { + pub fn upgrade(self, transaction: &Transaction<'_>) -> Result, Error> { match self { - Self::V0 => v0::upgrade(db).map(Some), - Self::V1 => Ok(None), + Self::V0 => v0::upgrade(transaction).map(Some), + Self::V1 => v1::upgrade(transaction).map(Some), + Self::V2 => Ok(None), } } } @@ -76,6 +80,7 @@ impl TryFrom for Version { match value { 0 => Ok(Version::V0), 1 => Ok(Version::V1), + 2 => Ok(Version::V2), v => Err(UnknownVersionError(v)), } } @@ -96,8 +101,8 @@ pub mod v0 { use super::*; /// Upgrade database from `v0` to `v1` - pub fn upgrade(db: &Database) -> Result { - v1::create_object_stores(db)?; + pub fn upgrade(transaction: &Transaction<'_>) -> Result { + v1::create_object_stores(transaction.db())?; Ok(Version::V1) } } @@ -208,4 +213,27 @@ pub mod v1 { .build()?; Ok(()) } + + /// Upgrade database from `v1` to `v2` + pub fn upgrade(transaction: &Transaction<'_>) -> Result { + v2::empty_leases(transaction)?; + Ok(Version::V2) + } +} + +mod v2 { + // Re-use all the same keys from `v1`. + pub use super::v1::keys; + use super::*; + + /// The format of [`Lease`][super::super::types::Lease] is changing. Let's + /// erase previous values. + pub fn empty_leases(transaction: &Transaction<'_>) -> Result<(), Error> { + let object_store = transaction.object_store(keys::LEASES)?; + + // Remove all previous leases. + object_store.clear()?; + + Ok(()) + } } diff --git a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs index 432298cc849..1a2dd26fd3d 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/mod.rs @@ -31,6 +31,10 @@ pub use error::IndexeddbMediaStoreError; use indexed_db_futures::{ cursor::CursorDirection, database::Database, transaction::TransactionMode, Build, }; +#[cfg(target_family = "wasm")] +use matrix_sdk_base::cross_process_lock::{ + CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION, +}; use matrix_sdk_base::{ media::{ store::{ @@ -106,28 +110,56 @@ impl MediaStore for IndexeddbMediaStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result, IndexeddbMediaStoreError> { let _timer = timer!("method"); - let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into()); - let transaction = self.transaction(&[Lease::OBJECT_STORE], TransactionMode::Readwrite)?; - if let Some(lease) = transaction.get_lease_by_id(key).await? { - if lease.holder != holder && !lease.has_expired(now) { - return Ok(false); + let now = Duration::from_millis(MilliSecondsSinceUnixEpoch::now().get().into()); + let expiration = now + Duration::from_millis(lease_duration_ms.into()); + + let lease = match transaction.get_lease_by_id(key).await? { + Some(mut lease) => { + if lease.holder == holder { + // We had the lease before, extend it. + lease.expiration = expiration; + + Some(lease) + } else { + // We didn't have it. + if lease.expiration < now { + // Steal it! + lease.holder = holder.to_owned(); + lease.expiration = expiration; + lease.generation += 1; + + Some(lease) + } else { + // We tried our best. + None + } + } } - } + None => { + let lease = Lease { + key: key.to_owned(), + holder: holder.to_owned(), + expiration, + generation: FIRST_CROSS_PROCESS_LOCK_GENERATION, + }; + + Some(lease) + } + }; - transaction - .put_lease(&Lease { - key: key.to_owned(), - holder: holder.to_owned(), - expiration: now + Duration::from_millis(lease_duration_ms.into()), - }) - .await?; - transaction.commit().await?; - Ok(true) + Ok(if let Some(lease) = lease { + transaction.put_lease(&lease).await?; + transaction.commit().await?; + + Some(lease.generation) + } else { + None + }) } #[instrument(skip_all)] diff --git a/crates/matrix-sdk-indexeddb/src/media_store/types.rs b/crates/matrix-sdk-indexeddb/src/media_store/types.rs index 6a1b2cb28a6..204afba9e92 100644 --- a/crates/matrix-sdk-indexeddb/src/media_store/types.rs +++ b/crates/matrix-sdk-indexeddb/src/media_store/types.rs @@ -17,7 +17,10 @@ use std::{ time::Duration, }; -use matrix_sdk_base::media::{store::IgnoreMediaRetentionPolicy, MediaRequestParameters}; +use matrix_sdk_base::{ + cross_process_lock::CrossProcessLockGeneration, + media::{store::IgnoreMediaRetentionPolicy, MediaRequestParameters}, +}; use ruma::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -29,13 +32,7 @@ pub struct Lease { pub key: String, pub holder: String, pub expiration: Duration, -} - -impl Lease { - /// Determines whether the lease is expired at a given time `t` - pub fn has_expired(&self, t: Duration) -> bool { - self.expiration < t - } + pub generation: CrossProcessLockGeneration, } /// A representation of media which ignores storage schemas. This is type is not diff --git a/crates/matrix-sdk-sqlite/Cargo.toml b/crates/matrix-sdk-sqlite/Cargo.toml index 83b7e945ecf..78c8119b43c 100644 --- a/crates/matrix-sdk-sqlite/Cargo.toml +++ b/crates/matrix-sdk-sqlite/Cargo.toml @@ -15,7 +15,7 @@ default = ["state-store", "event-cache"] testing = ["matrix-sdk-crypto?/testing"] bundled = ["rusqlite/bundled"] -crypto-store = ["dep:matrix-sdk-crypto"] +crypto-store = ["dep:matrix-sdk-base", "dep:matrix-sdk-crypto"] event-cache = ["dep:matrix-sdk-base"] state-store = ["dep:matrix-sdk-base"] diff --git a/crates/matrix-sdk-sqlite/migrations/crypto_store/013_lease_locks_with_generation.sql b/crates/matrix-sdk-sqlite/migrations/crypto_store/013_lease_locks_with_generation.sql new file mode 100644 index 00000000000..5c67fcb8bcf --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/crypto_store/013_lease_locks_with_generation.sql @@ -0,0 +1,7 @@ +-- Rename the `expiration_ts` column to `expiration` to be consistent with other +-- `lease_locks` tables in other stores. +ALTER TABLE "lease_locks" RENAME COLUMN "expiration_ts" TO "expiration"; + +-- Add the `generation` column to handle _dirtiness. +-- Default value is `FIRST_CROSS_PROCESS_LOCK_GENERATION`. +ALTER TABLE "lease_locks" ADD COLUMN "generation" INTEGER NOT NULL DEFAULT 1; diff --git a/crates/matrix-sdk-sqlite/migrations/event_cache_store/013_lease_locks_with_generation.sql b/crates/matrix-sdk-sqlite/migrations/event_cache_store/013_lease_locks_with_generation.sql new file mode 100644 index 00000000000..70496cebc24 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/event_cache_store/013_lease_locks_with_generation.sql @@ -0,0 +1,3 @@ +-- Add the `generation` column to handle _dirtiness. +-- Default value is `FIRST_CROSS_PROCESS_LOCK_GENERATION`. +ALTER TABLE "lease_locks" ADD COLUMN "generation" INTEGER NOT NULL DEFAULT 1; diff --git a/crates/matrix-sdk-sqlite/migrations/media_store/002_lease_locks_with_generation.sql b/crates/matrix-sdk-sqlite/migrations/media_store/002_lease_locks_with_generation.sql new file mode 100644 index 00000000000..e65cbbb5fd6 --- /dev/null +++ b/crates/matrix-sdk-sqlite/migrations/media_store/002_lease_locks_with_generation.sql @@ -0,0 +1,3 @@ +-- Add the `generation` column to handle _dirtiness_. +-- Default value is `FIRST_CROSS_PROCESS_LOCK_GENERATION`. +ALTER TABLE "lease_locks" ADD COLUMN "generation" INTEGER NOT NULL DEFAULT 1; diff --git a/crates/matrix-sdk-sqlite/src/crypto_store.rs b/crates/matrix-sdk-sqlite/src/crypto_store.rs index 03ca2d22d25..558b30e1170 100644 --- a/crates/matrix-sdk-sqlite/src/crypto_store.rs +++ b/crates/matrix-sdk-sqlite/src/crypto_store.rs @@ -21,6 +21,7 @@ use std::{ use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; +use matrix_sdk_base::{cross_process_lock::CrossProcessLockGeneration, timer}; use matrix_sdk_crypto::{ olm::{ InboundGroupSession, OutboundGroupSession, PickledInboundGroupSession, @@ -175,7 +176,7 @@ impl SqliteCryptoStore { } } -const DATABASE_VERSION: u8 = 12; +const DATABASE_VERSION: u8 = 13; /// key for the dehydrated device pickle key in the key/value table. const DEHYDRATED_DEVICE_PICKLE_KEY: &str = "dehydrated_device_pickle_key"; @@ -301,6 +302,16 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 13 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!( + "../migrations/crypto_store/013_lease_locks_with_generation.sql" + ))?; + txn.set_db_version(13) + }) + .await?; + } + Ok(()) } @@ -1488,37 +1499,52 @@ impl CryptoStore for SqliteCryptoStore { Ok(()) } + #[instrument(skip(self))] async fn try_take_leased_lock( &self, lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result> { + let _timer = timer!("method"); + let key = key.to_owned(); let holder = holder.to_owned(); - let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); - let expiration_ts = now_ts + lease_duration_ms as u64; + let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); + let expiration = now + lease_duration_ms as u64; - let num_touched = self + // Learn about the `excluded` keyword in https://sqlite.org/lang_upsert.html. + let generation = self .acquire() .await? .with_transaction(move |txn| { - txn.execute( - "INSERT INTO lease_locks (key, holder, expiration_ts) + txn.query_row( + "INSERT INTO lease_locks (key, holder, expiration) VALUES (?1, ?2, ?3) ON CONFLICT (key) DO - UPDATE SET holder = ?2, expiration_ts = ?3 - WHERE holder = ?2 - OR expiration_ts < ?4 - ", - (key, holder, expiration_ts, now_ts), + UPDATE SET + holder = excluded.holder, + expiration = excluded.expiration, + generation = + CASE holder + WHEN excluded.holder THEN generation + ELSE generation + 1 + END + WHERE + holder = excluded.holder + OR expiration < ?4 + RETURNING generation + ", + (key, holder, expiration, now), + |row| row.get(0), ) + .optional() }) .await?; - Ok(num_touched == 1) + Ok(generation) } async fn next_batch_token(&self) -> Result, Self::Error> { diff --git a/crates/matrix-sdk-sqlite/src/event_cache_store.rs b/crates/matrix-sdk-sqlite/src/event_cache_store.rs index d029c40fc61..4fb20222cc2 100644 --- a/crates/matrix-sdk-sqlite/src/event_cache_store.rs +++ b/crates/matrix-sdk-sqlite/src/event_cache_store.rs @@ -19,6 +19,7 @@ use std::{collections::HashMap, fmt, iter::once, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ + cross_process_lock::CrossProcessLockGeneration, deserialized_responses::TimelineEvent, event_cache::{ store::{extract_event_relation, EventCacheStore}, @@ -66,7 +67,7 @@ const DATABASE_NAME: &str = "matrix-sdk-event-cache.sqlite3"; /// This is used to figure whether the SQLite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`run_migrations`] function. -const DATABASE_VERSION: u8 = 12; +const DATABASE_VERSION: u8 = 13; /// The string used to identify a chunk of type events, in the `type` field in /// the database. @@ -485,6 +486,16 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 13 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!( + "../migrations/event_cache_store/013_lease_locks_with_generation.sql" + ))?; + txn.set_db_version(13) + }) + .await?; + } + Ok(()) } @@ -498,7 +509,7 @@ impl EventCacheStore for SqliteEventCacheStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result> { let _timer = timer!("method"); let key = key.to_owned(); @@ -507,25 +518,37 @@ impl EventCacheStore for SqliteEventCacheStore { let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); let expiration = now + lease_duration_ms as u64; - let num_touched = self + // Learn about the `excluded` keyword in https://sqlite.org/lang_upsert.html. + let generation = self .write() .await? .with_transaction(move |txn| { - txn.execute( + txn.query_row( "INSERT INTO lease_locks (key, holder, expiration) VALUES (?1, ?2, ?3) ON CONFLICT (key) DO - UPDATE SET holder = ?2, expiration = ?3 - WHERE holder = ?2 - OR expiration < ?4 - ", + UPDATE SET + holder = excluded.holder, + expiration = excluded.expiration, + generation = + CASE holder + WHEN excluded.holder THEN generation + ELSE generation + 1 + END + WHERE + holder = excluded.holder + OR expiration < ?4 + RETURNING generation + ", (key, holder, expiration, now), + |row| row.get(0), ) + .optional() }) .await?; - Ok(num_touched == 1) + Ok(generation) } #[instrument(skip(self, updates))] diff --git a/crates/matrix-sdk-sqlite/src/media_store.rs b/crates/matrix-sdk-sqlite/src/media_store.rs index 1bcc4d860ea..a7275f55055 100644 --- a/crates/matrix-sdk-sqlite/src/media_store.rs +++ b/crates/matrix-sdk-sqlite/src/media_store.rs @@ -19,6 +19,7 @@ use std::{fmt, path::Path, sync::Arc}; use async_trait::async_trait; use deadpool_sqlite::{Object as SqliteAsyncConn, Pool as SqlitePool, Runtime}; use matrix_sdk_base::{ + cross_process_lock::CrossProcessLockGeneration, media::{ store::{ IgnoreMediaRetentionPolicy, MediaRetentionPolicy, MediaService, MediaStore, @@ -63,7 +64,7 @@ const DATABASE_NAME: &str = "matrix-sdk-media.sqlite3"; /// This is used to figure whether the SQLite database requires a migration. /// Every new SQL migration should imply a bump of this number, and changes in /// the [`run_migrations`] function. -const DATABASE_VERSION: u8 = 1; +const DATABASE_VERSION: u8 = 2; /// An SQLite-based media store. #[derive(Clone)] @@ -225,6 +226,16 @@ async fn run_migrations(conn: &SqliteAsyncConn, version: u8) -> Result<()> { .await?; } + if version < 2 { + conn.with_transaction(|txn| { + txn.execute_batch(include_str!( + "../migrations/media_store/002_lease_locks_with_generation.sql" + ))?; + txn.set_db_version(2) + }) + .await?; + } + Ok(()) } @@ -238,7 +249,7 @@ impl MediaStore for SqliteMediaStore { lease_duration_ms: u32, key: &str, holder: &str, - ) -> Result { + ) -> Result> { let _timer = timer!("method"); let key = key.to_owned(); @@ -247,25 +258,37 @@ impl MediaStore for SqliteMediaStore { let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into(); let expiration = now + lease_duration_ms as u64; - let num_touched = self + // Learn about the `excluded` keyword in https://sqlite.org/lang_upsert.html. + let generation = self .write() .await? .with_transaction(move |txn| { - txn.execute( + txn.query_row( "INSERT INTO lease_locks (key, holder, expiration) VALUES (?1, ?2, ?3) ON CONFLICT (key) DO - UPDATE SET holder = ?2, expiration = ?3 - WHERE holder = ?2 - OR expiration < ?4 - ", + UPDATE SET + holder = excluded.holder, + expiration = excluded.expiration, + generation = + CASE holder + WHEN excluded.holder THEN generation + ELSE generation + 1 + END + WHERE + holder = excluded.holder + OR expiration < ?4 + RETURNING generation + ", (key, holder, expiration, now), + |row| row.get(0), ) + .optional() }) .await?; - Ok(num_touched == 1) + Ok(generation) } async fn add_media_content( diff --git a/crates/matrix-sdk/src/encryption/mod.rs b/crates/matrix-sdk/src/encryption/mod.rs index 21bca38fc5a..78d4d5f64de 100644 --- a/crates/matrix-sdk/src/encryption/mod.rs +++ b/crates/matrix-sdk/src/encryption/mod.rs @@ -1698,8 +1698,9 @@ impl Encryption { // If we don't get the lock immediately, then it is already acquired by another // process, and we'll get to reload next time we acquire the lock. { - let guard = lock.try_lock_once().await?; - if guard.is_some() { + let lock_result = lock.try_lock_once().await?; + + if lock_result.is_ok() { olm_machine .initialize_crypto_store_generation( &self.client.locks().crypto_store_generation, @@ -1773,9 +1774,9 @@ impl Encryption { &self, ) -> Result, Error> { if let Some(lock) = self.client.locks().cross_process_crypto_store_lock.get() { - let maybe_guard = lock.try_lock_once().await?; + let lock_result = lock.try_lock_once().await?; - let Some(guard) = maybe_guard else { + let Some(guard) = lock_result.ok() else { return Ok(None); };