Skip to main content

matrix_sdk_common/
cross_process_lock.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! A cross-process lock implementation.
16//!
17//! This is a per-process lock that may be used only for very specific use
18//! cases, where multiple processes might concurrently write to the same
19//! database at the same time; this would invalidate store caches, so
20//! that should be done mindfully. Such a lock can be obtained multiple times by
21//! the same process, and it remains active as long as there's at least one user
22//! in a given process.
23//!
24//! The lock is implemented using time-based leases. The lock maintains the lock
25//! identifier (key), who's the current holder (value), and an expiration
26//! timestamp on the side; see also `CryptoStore::try_take_leased_lock` for more
27//! details.
28//!
29//! The lock is initially obtained for a certain period of time (namely, the
30//! duration of a lease, aka `LEASE_DURATION_MS`), and then a “heartbeat” task
31//! renews the lease to extend its duration, every so often (namely, every
32//! `EXTEND_LEASE_EVERY_MS`). Since the Tokio scheduler might be busy, the
33//! extension request should happen way more frequently than the duration of a
34//! lease, in case a deadline is missed. The current values have been chosen to
35//! reflect that, with a ratio of 1:10 as of 2023-06-23.
36//!
37//! Releasing the lock happens naturally, by not renewing a lease. It happens
38//! automatically after the duration of the last lease, at most.
39
40use std::{
41    error::Error,
42    future::Future,
43    sync::{
44        Arc, Weak,
45        atomic::{AtomicBool, AtomicU64, Ordering},
46    },
47    time::Duration,
48};
49
50use tokio::sync::Mutex;
51use tracing::{debug, error, instrument, trace, warn};
52
53use crate::{
54    SendOutsideWasm, SyncOutsideWasm,
55    executor::{AbortOnDrop, JoinHandleExt, spawn},
56    sleep::sleep,
57};
58
59/// A lock generation is an integer incremented each time the lock is taken by a
60/// different holder.
61///
62/// This is used to know if a lock has been dirtied.
63pub type CrossProcessLockGeneration = u64;
64
65/// A trait that represents any function which can be used to acquire the
66/// underlying lock of a [`CrossProcessLock`].
67///
68/// For example, this can be useful when writing a function which is
69/// parameterized to acquire the underlying lock through either
70/// [`CrossProcessLock::spin_lock`] or [`CrossProcessLock::try_lock_once`].
71pub trait AcquireCrossProcessLockFn<L>
72where
73    Self: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
74    L: TryLock + Clone + SendOutsideWasm + 'static,
75{
76}
77
78impl<L, T> AcquireCrossProcessLockFn<L> for T
79where
80    T: AsyncFn(&CrossProcessLock<L>) -> AcquireCrossProcessLockResult<L::LockError>,
81    L: TryLock + Clone + SendOutsideWasm + 'static,
82{
83}
84
85/// A convenience type for the [`Result`] returned from calling or
86/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
87pub type AcquireCrossProcessLockResult<E> =
88    Result<Result<CrossProcessLockState, CrossProcessLockUnobtained>, E>;
89
90/// Trait used to try to take a lock. Foundation of [`CrossProcessLock`].
91pub trait TryLock {
92    #[cfg(not(target_family = "wasm"))]
93    type LockError: Error + Send + Sync;
94
95    #[cfg(target_family = "wasm")]
96    type LockError: Error;
97
98    /// Try to take a leased lock.
99    ///
100    /// This attempts to take a lock for the given lease duration.
101    ///
102    /// - If we already had the lease, this will extend the lease.
103    /// - If we didn't, but the previous lease has expired, we will obtain the
104    ///   lock.
105    /// - If there was no previous lease, we will obtain the lock.
106    /// - Otherwise, we don't get the lock.
107    ///
108    /// Returns `Some(_)` to indicate the lock succeeded, `None` otherwise. The
109    /// cross-process lock generation must be compared to the generation before
110    /// the call to see if the lock has been dirtied: a different generation
111    /// means the lock has been dirtied, i.e. taken by a different holder in the
112    /// meantime.
113    fn try_lock(
114        &self,
115        lease_duration_ms: u32,
116        key: &str,
117        holder: &str,
118    ) -> impl Future<Output = Result<Option<CrossProcessLockGeneration>, Self::LockError>>
119    + SendOutsideWasm;
120}
121
122/// Small state machine to handle wait times.
123#[derive(Clone, Debug)]
124enum WaitingTime {
125    /// Some time to wait, in milliseconds.
126    Some(u32),
127    /// Stop waiting when seeing this value.
128    Stop,
129}
130
131/// A guard of a cross-process lock.
132///
133/// The lock will be automatically released a short period of time after all the
134/// guards have dropped.
135#[derive(Clone, Debug)]
136#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
137pub struct CrossProcessLockGuard {
138    /// A clone of [`CrossProcessLock::inner`].
139    ///
140    /// The number of guards/holders is based on the `Weak::weak_count`.
141    ///
142    /// - Every time [`CrossProcessLockGuard`] is cloned, `Weak` is cloned, and
143    ///   thus the number of holders of `CrossProcessLockInner` increases.
144    /// - Every time [`CrossProcessLockGuard`] is dropped, `Weak` is dropped,
145    ///   and thus the number of holders of `CrossProcessLockInner` decreases.
146    inner: Weak<CrossProcessLockInner>,
147}
148
149impl CrossProcessLockGuard {
150    fn new(inner: &Arc<CrossProcessLockInner>) -> Self {
151        // Downgrading the strong pointer to a weak pointer to represent a new
152        // lock holder.
153        Self { inner: Arc::downgrade(inner) }
154    }
155
156    /// Determine whether the cross-process lock associated to this guard is
157    /// dirty.
158    ///
159    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
160    /// of _dirty_.
161    pub fn is_dirty(&self) -> bool {
162        self.inner
163            .upgrade()
164            .map(|inner| inner.is_dirty())
165            // If it's not possible to upgrade the weak pointer, it means the
166            // lock _and_ the `renew_task` have been dropped. In this case,
167            // whether the lock is dirty or not doesn't make any difference.
168            .unwrap_or(false)
169    }
170
171    /// Clear the dirty state from the cross-process lock associated to this
172    /// guard.
173    ///
174    /// If the cross-process lock is dirtied, it will remain dirtied until this
175    /// method is called. This allows recovering from a dirty state and marking
176    /// that it has recovered.
177    pub fn clear_dirty(&self) {
178        // If it's not possible to upgrade the weak pointer, it means the lock
179        // _and_ the `renew_task` have been dropped. Marking the lock as
180        // non-dirty makes no particular sense, so we do nothing.
181        if let Some(inner) = self.inner.upgrade() {
182            inner.clear_dirty();
183        }
184    }
185
186    #[cfg(test)]
187    fn count_holders(inner: &Weak<CrossProcessLockInner>) -> usize {
188        Weak::weak_count(inner)
189    }
190}
191
192/// A cross-process lock implementation.
193///
194/// See the doc-comment of this module for more information.
195#[derive(Clone, Debug)]
196pub struct CrossProcessLock<L> {
197    /// The locker implementation.
198    ///
199    /// `L` is responsible for trying to take the lock, while
200    /// [`CrossProcessLock`] is responsible to make it cross-process, with the
201    /// retry mechanism, plus guard and so on.
202    locker: Arc<L>,
203
204    /// The inner data of the lock, shared with all the lock holders.
205    ///
206    /// The number of lock holders must be computed with
207    /// [`CrossProcessLock::count_holders`].
208    ///
209    /// If the number of lock holders is greater than 0, this means we've
210    /// already obtained this lock, in this process, and the store lock mustn't
211    /// be touched.
212    ///
213    /// When the number of holders is decreased to 0, then the lock must be
214    /// released in the store.
215    // Notes about the `Arc`/`Weak` usage:
216    //
217    // - We want to track the number of holders, i.e. the number of guards. To achieve that, we
218    //   could use a thread-safe counter, or hijack `Arc` and `Weak` which provide two thread-safe
219    //   counters: strong count and weak count.
220    // - `CrossProcessLock` holds an `Arc` (this field).
221    // - `renew_task` holds an `Arc` (a clone of this field).
222    // - `CrossProcessLockGuard` holds a `Weak` (it could use an `Arc`, but a `Weak` is fine in
223    //   this context and offers a unique counter for guards!).
224    // - Counting holders = counting the number of `Weak` pointers.
225    // - It is safe to upgrade the `Weak` pointer to an `Arc` (to get information about dirtiness)
226    //   in a guard because the `renew_task` holds a clone of the `Arc` and will not exit until all
227    //   guards have been dropped.
228    // - It is always possible to create a `Weak` pointer (i) either from `CrossProcessLock` by
229    //   using `Arc::downgrade`, (ii) or from `CrossProcessLockGuard` by cloning it.
230    inner: Arc<CrossProcessLockInner>,
231
232    /// The key used in the key/value mapping for the lock entry.
233    lock_key: String,
234
235    /// A mutex to control an attempt to take the lock, to prevent someone using
236    /// it in a re-entrant way.
237    locking_attempt: Arc<Mutex<()>>,
238
239    /// Backoff time, in milliseconds.
240    backoff: Arc<Mutex<WaitingTime>>,
241
242    /// The cross-process lock configuration.
243    config: CrossProcessLockConfig,
244}
245
246/// Inner data for [`CrossProcessLock`] and [`CrossProcessLockGuard`].
247#[derive(Debug)]
248struct CrossProcessLockInner {
249    /// Current renew task spawned by [`CrossProcessLock::try_lock_once`].
250    ///
251    /// It is not used directly by [`CrossProcessLockGuard`]. It is stored here
252    /// to ensure the task will drop once the lock and all the guards drop.
253    renew_task: Mutex<Option<AbortOnDrop<()>>>,
254
255    /// This lock generation.
256    generation: AtomicU64,
257
258    /// Whether the lock has been dirtied.
259    ///
260    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
261    /// of _dirty_.
262    is_dirty: AtomicBool,
263}
264
265impl CrossProcessLockInner {
266    /// Determine whether the cross-process lock is dirty.
267    ///
268    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
269    /// of _dirty_.
270    pub fn is_dirty(&self) -> bool {
271        self.is_dirty.load(Ordering::SeqCst)
272    }
273
274    /// Clear the dirty state from this cross-process lock.
275    ///
276    /// If the cross-process lock is dirtied, it will remain dirtied until this
277    /// method is called. This allows recovering from a dirty state and marking
278    /// that it has recovered.
279    pub fn clear_dirty(&self) {
280        self.is_dirty.store(false, Ordering::SeqCst);
281    }
282}
283
284/// Amount of time a lease of the lock should last, in milliseconds.
285pub const LEASE_DURATION_MS: u32 = 500;
286
287/// Period of time between two attempts to extend the lease. We'll re-request a
288/// lease for an entire duration of `LEASE_DURATION_MS` milliseconds, every
289/// `EXTEND_LEASE_EVERY_MS`, so this has to be an amount safely low compared to
290/// `LEASE_DURATION_MS`, to make sure that we can miss a deadline without
291/// compromising the lock.
292pub const EXTEND_LEASE_EVERY_MS: u64 = 50;
293
294/// Initial backoff, in milliseconds. This is the time we wait the first time,
295/// if taking the lock initially failed.
296const INITIAL_BACKOFF_MS: u32 = 10;
297
298/// Maximal backoff, in milliseconds. This is the maximum amount of time we'll
299/// wait for the lock, _between two attempts_.
300pub const MAX_BACKOFF_MS: u32 = 1000;
301
302/// Sentinel value representing the absence of a lock generation value.
303///
304/// When the lock is created, it has no generation. Once locked, it receives its
305/// first generation from [`TryLock::try_lock`]. Subsequent lockings may
306/// generate new lock generation. The generation is incremented by 1 every time.
307///
308/// The first generation is defined by [`FIRST_CROSS_PROCESS_LOCK_GENERATION`].
309pub const NO_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 0;
310
311/// Describe the first lock generation value (see
312/// [`CrossProcessLockGeneration`]).
313pub const FIRST_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 1;
314
315impl<L> CrossProcessLock<L>
316where
317    L: TryLock + Clone + SendOutsideWasm + SyncOutsideWasm + 'static,
318{
319    /// Create a new cross-process lock.
320    ///
321    /// # Parameters
322    ///
323    /// - `lock_key`: key in the key-value store to store the lock's state.
324    /// - `config`: the cross-process lock configuration to use, if it's
325    ///   [`CrossProcessLockConfig::SingleProcess`], no actual lock will be
326    ///   taken.
327    pub fn new(locker: L, lock_key: String, config: CrossProcessLockConfig) -> Self {
328        Self {
329            locker: Arc::new(locker),
330            lock_key,
331            locking_attempt: Arc::new(Mutex::new(())),
332            inner: Arc::new(CrossProcessLockInner {
333                renew_task: Default::default(),
334                generation: AtomicU64::new(NO_CROSS_PROCESS_LOCK_GENERATION),
335
336                is_dirty: AtomicBool::new(false),
337            }),
338            backoff: Arc::new(Mutex::new(WaitingTime::Some(INITIAL_BACKOFF_MS))),
339            config,
340        }
341    }
342
343    /// Count the number of holders.
344    ///
345    /// # Safety
346    ///
347    /// This method by itself is safe, but using it correctly requires extra
348    /// care. Another thread can change the weak count at any time, including
349    /// potentially between calling this method and acting on the result.
350    fn count_holders(inner: &Arc<CrossProcessLockInner>) -> usize {
351        Arc::weak_count(inner)
352    }
353
354    /// Determine whether the cross-process lock is dirty.
355    ///
356    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
357    /// of _dirty_.
358    pub fn is_dirty(&self) -> bool {
359        self.inner.is_dirty()
360    }
361
362    /// Clear the dirty state from this cross-process lock.
363    ///
364    /// If the cross-process lock is dirtied, it will remain dirtied until this
365    /// method is called. This allows recovering from a dirty state and marking
366    /// that it has recovered.
367    pub fn clear_dirty(&self) {
368        self.inner.clear_dirty();
369    }
370
371    /// Try to lock once, returns whether the lock was obtained or not.
372    ///
373    /// The lock can be obtained but it can be dirty. In all cases, the renew
374    /// task will run in the background.
375    #[instrument(skip(self), fields(?self.lock_key, ?self.config, ?self.inner.generation))]
376    pub async fn try_lock_once(&self) -> AcquireCrossProcessLockResult<L::LockError> {
377        // If it's not `MultiProcess`, this behaves as a no-op
378        let CrossProcessLockConfig::MultiProcess { holder_name } = &self.config else {
379            let guard = CrossProcessLockGuard::new(&self.inner);
380            return Ok(Ok(CrossProcessLockState::Clean(guard)));
381        };
382
383        // Hold onto the locking attempt mutex for the entire lifetime of this
384        // function, to avoid multiple reentrant calls.
385        let mut _attempt = self.locking_attempt.lock().await;
386
387        // If there is at least one other holder, it means the lock has already
388        // been acquired, and we can safely generate a new guard.
389        if Self::count_holders(&self.inner) > 0 {
390            // Note: between the above “count” and the
391            // `CrossProcessLockGuard::new` below, another thread may decrement
392            // the number of holders. That's fine because that means the lock
393            // was taken by at least one thread, and after this call it will be
394            // taken by at least one thread.
395            //
396            // Because `locking_attempt` is acquired, the task cannot drop the
397            // lock while the “count” might change.
398            trace!("We already had the lock, incrementing holder count");
399
400            return Ok(Ok(CrossProcessLockState::Clean(CrossProcessLockGuard::new(&self.inner))));
401        }
402
403        if let Some(new_generation) =
404            self.locker.try_lock(LEASE_DURATION_MS, &self.lock_key, holder_name).await?
405        {
406            match self.inner.generation.swap(new_generation, Ordering::SeqCst) {
407                // If there was no lock generation, it means this is the first
408                // time the lock is obtained. It cannot be dirty.
409                NO_CROSS_PROCESS_LOCK_GENERATION => {
410                    trace!(?new_generation, "Setting the lock generation for the first time");
411                }
412
413                // This was NOT the same generation, the lock has been dirtied!
414                previous_generation if previous_generation != new_generation => {
415                    warn!(
416                        ?previous_generation,
417                        ?new_generation,
418                        "The lock has been obtained, but it's been dirtied!"
419                    );
420                    self.inner.is_dirty.store(true, Ordering::SeqCst);
421                }
422
423                // This was the same generation, no problem.
424                _ => {
425                    trace!("Same lock generation; no problem");
426                }
427            }
428
429            trace!("Lock obtained!");
430        } else {
431            trace!("Couldn't obtain the lock immediately.");
432            return Ok(Err(CrossProcessLockUnobtained::Busy));
433        }
434
435        trace!("Obtained the lock, spawning the lease extension task.");
436
437        // No lock was acquired before (either because it's the first time the
438        // lock is acquired, or because all previous guards have been dropped).
439        // We're going to spawn the task that will renew the lease.
440
441        let mut renew_task = self.inner.renew_task.lock().await;
442
443        // Cancel the previous task, if any. That's safe to do, because:
444        //
445        // - either the task was done,
446        // - or it was still running, but taking a lock in the database has to
447        //   be an atomic operation running in a transaction.
448        drop(renew_task.take());
449
450        // Restart a new one.
451        *renew_task = Some(
452            spawn({
453                let locker = self.locker.clone();
454                let lock_key = self.lock_key.clone();
455                let locking_attempt = self.locking_attempt.clone();
456                let config = self.config.clone();
457
458                // By cloning `CrossProcessLockInner`, we ensure the task acts
459                // as a lock holder.
460                let inner = self.inner.clone();
461
462                async move {
463                    let CrossProcessLockConfig::MultiProcess { holder_name } = config else {
464                        return;
465                    };
466
467                    loop {
468                        {
469                            // First, check if there are still users of this
470                            // lock.
471                            //
472                            // This is not racy, because:
473                            //
474                            // - the `locking_attempt` mutex makes sure we don't
475                            //   have unexpected interactions with the
476                            //   non-atomic sequence above in `try_lock_once`,
477                            // - other holders will only decrease over time.
478
479                            let _guard = locking_attempt.lock().await;
480
481                            // There are no more holders. We can quit.
482                            if Self::count_holders(&inner) == 0 {
483                                trace!("exiting the lease extension loop");
484
485                                // Cancel the lease with another 0ms lease. If
486                                // we don't get the lock, that's (weird but)
487                                // fine.
488                                let fut = locker.try_lock(0, &lock_key, &holder_name);
489                                let _ = fut.await;
490
491                                // Exit the loop.
492                                break;
493                            }
494                        }
495
496                        sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await;
497
498                        match locker.try_lock(LEASE_DURATION_MS, &lock_key, &holder_name).await {
499                            Ok(Some(_generation)) => {
500                                // It's impossible that the generation can be
501                                // different from the previous generation.
502                                //
503                                // As long as the task runs, the lock is
504                                // renewed, so the generation remains the same.
505                                // If the lock is not taken, it's because the
506                                // lease has expired, which is represented by
507                                // the `Ok(None)` value, and the task must stop.
508                            }
509
510                            Ok(None) => {
511                                error!(
512                                    "Failed to renew the lock lease: the lock could not be obtained"
513                                );
514
515                                // Exit the loop.
516                                break;
517                            }
518
519                            Err(err) => {
520                                error!("Error when extending the lock lease: {err:#}");
521
522                                // Exit the loop.
523                                break;
524                            }
525                        }
526                    }
527                }
528            })
529            .abort_on_drop(),
530        );
531
532        let guard = CrossProcessLockGuard::new(&self.inner);
533
534        Ok(Ok(if self.is_dirty() {
535            CrossProcessLockState::Dirty(guard)
536        } else {
537            CrossProcessLockState::Clean(guard)
538        }))
539    }
540
541    /// Attempt to take the lock, with exponential backoff if the lock has
542    /// already been taken before.
543    ///
544    /// The `max_backoff` parameter is the maximum time (in milliseconds) that
545    /// should be waited for, between two attempts. When that time is reached a
546    /// second time, the lock will stop attempting to get the lock and will
547    /// return a timeout error upon locking. If not provided, will wait for
548    /// [`MAX_BACKOFF_MS`].
549    #[instrument(skip(self), fields(?self.lock_key, ?self.config))]
550    pub async fn spin_lock(
551        &self,
552        max_backoff: Option<u32>,
553    ) -> AcquireCrossProcessLockResult<L::LockError> {
554        // If there is no holder, this behaves as a no-op
555        let max_backoff = max_backoff.unwrap_or(MAX_BACKOFF_MS);
556
557        // Note: reads/writes to the backoff are racy across threads in theory,
558        // but the lock in `try_lock_once` should sequentialize it all.
559
560        loop {
561            // If the cross-process lock config is not `MultiProcess`, this
562            // behaves as a no-op and we just return
563            let lock_result = self.try_lock_once().await?;
564
565            if lock_result.is_ok() {
566                if matches!(self.config, CrossProcessLockConfig::MultiProcess { .. }) {
567                    // Reset backoff before returning, for the next attempt to
568                    // lock.
569                    *self.backoff.lock().await = WaitingTime::Some(INITIAL_BACKOFF_MS);
570                }
571
572                return Ok(lock_result);
573            }
574
575            // Exponential backoff! Multiply by 2 the time we've waited before,
576            // cap it to max_backoff.
577            let mut backoff = self.backoff.lock().await;
578
579            let wait = match &mut *backoff {
580                WaitingTime::Some(val) => {
581                    let wait = *val;
582                    *val = val.saturating_mul(2);
583                    if *val >= max_backoff {
584                        *backoff = WaitingTime::Stop;
585                    }
586                    wait
587                }
588                WaitingTime::Stop => {
589                    // We've reached the maximum backoff, abandon.
590                    return Ok(Err(CrossProcessLockUnobtained::TimedOut));
591                }
592            };
593
594            debug!("Waiting {wait} before re-attempting to take the lock");
595            sleep(Duration::from_millis(wait.into())).await;
596        }
597    }
598
599    /// Returns the value in the database that represents the holder's
600    /// identifier.
601    pub fn lock_holder(&self) -> Option<&str> {
602        self.config.holder_name()
603    }
604}
605
606/// Represent a successful result of a locking attempt, either by
607/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
608#[derive(Debug)]
609#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
610pub enum CrossProcessLockState {
611    /// The lock has been obtained successfully, all good.
612    Clean(CrossProcessLockGuard),
613
614    /// The lock has been obtained successfully, but the lock is dirty!
615    ///
616    /// This holder has obtained this cross-process lock once, then another
617    /// holder has obtained this cross-process lock _before_ this holder
618    /// obtained it again. The lock is marked as dirty. It means the value
619    /// protected by the cross-process lock may need to be reloaded if
620    /// synchronisation is important.
621    ///
622    /// Until [`CrossProcessLock::clear_dirty`] is called,
623    /// [`CrossProcessLock::is_dirty`], [`CrossProcessLock::try_lock_once`] and
624    /// [`CrossProcessLock::spin_lock`] will report the lock as dirty. Put it
625    /// differently: dirty once, dirty forever, unless
626    /// [`CrossProcessLock::clear_dirty`] is called.
627    Dirty(CrossProcessLockGuard),
628}
629
630impl CrossProcessLockState {
631    /// Map this value into the inner [`CrossProcessLockGuard`].
632    pub fn into_guard(self) -> CrossProcessLockGuard {
633        match self {
634            Self::Clean(guard) | Self::Dirty(guard) => guard,
635        }
636    }
637
638    /// Map this [`CrossProcessLockState`] into a
639    /// [`MappedCrossProcessLockState`].
640    ///
641    /// This is helpful when one wants to create its own wrapper over
642    /// [`CrossProcessLockGuard`].
643    pub fn map<F, G>(self, mapper: F) -> MappedCrossProcessLockState<G>
644    where
645        F: FnOnce(CrossProcessLockGuard) -> G,
646    {
647        match self {
648            Self::Clean(guard) => MappedCrossProcessLockState::Clean(mapper(guard)),
649            Self::Dirty(guard) => MappedCrossProcessLockState::Dirty(mapper(guard)),
650        }
651    }
652}
653
654/// A mapped [`CrossProcessLockState`].
655///
656/// Created by [`CrossProcessLockState::map`].
657#[derive(Debug)]
658#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
659pub enum MappedCrossProcessLockState<G> {
660    /// The equivalent of [`CrossProcessLockState::Clean`].
661    Clean(G),
662
663    /// The equivalent of [`CrossProcessLockState::Dirty`].
664    Dirty(G),
665}
666
667impl<G> MappedCrossProcessLockState<G> {
668    /// Return `Some(G)` if `Self` is [`Clean`][Self::Clean].
669    pub fn as_clean(&self) -> Option<&G> {
670        match self {
671            Self::Clean(guard) => Some(guard),
672            Self::Dirty(_) => None,
673        }
674    }
675}
676
677/// Represent an unsuccessful result of a lock attempt, either by
678/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
679#[derive(Clone, Debug, thiserror::Error)]
680pub enum CrossProcessLockUnobtained {
681    /// The lock couldn't be obtained immediately because it is busy, i.e. it is
682    /// held by another holder.
683    #[error(
684        "The lock couldn't be obtained immediately because it is busy, i.e. it is held by another holder"
685    )]
686    Busy,
687
688    /// The lock couldn't be obtained after several attempts: locking has timed
689    /// out.
690    #[error("The lock couldn't be obtained after several attempts: locking has timed out")]
691    TimedOut,
692}
693
694/// Union of [`CrossProcessLockUnobtained`] and [`TryLock::LockError`].
695#[derive(Clone, Debug, thiserror::Error)]
696pub enum CrossProcessLockError {
697    #[error(transparent)]
698    Unobtained(#[from] CrossProcessLockUnobtained),
699
700    #[error(transparent)]
701    #[cfg(not(target_family = "wasm"))]
702    TryLock(#[from] Arc<dyn Error + Send + Sync>),
703
704    #[error(transparent)]
705    #[cfg(target_family = "wasm")]
706    TryLock(#[from] Arc<dyn Error>),
707}
708
709/// The cross-process lock config to use for the various stores.
710#[derive(Clone, Debug)]
711pub enum CrossProcessLockConfig {
712    /// The stores will be used in multiple processes, the holder name for the
713    /// cross-process lock is the associated `String`.
714    MultiProcess {
715        /// The name of the holder of the cross-process lock.
716        holder_name: String,
717    },
718    /// The stores will be used in a single process, there is no need for a
719    /// cross-process lock.
720    SingleProcess,
721}
722
723impl CrossProcessLockConfig {
724    /// Helper for quickly creating a [`CrossProcessLockConfig::MultiProcess`]
725    /// variant.
726    pub fn multi_process(holder_name: impl Into<String>) -> Self {
727        Self::MultiProcess { holder_name: holder_name.into() }
728    }
729
730    /// The holder name for the cross-process lock. This is only relevant for
731    /// [`CrossProcessLockConfig::MultiProcess`] variants.
732    pub fn holder_name(&self) -> Option<&str> {
733        match self {
734            Self::MultiProcess { holder_name } => Some(holder_name),
735            Self::SingleProcess => None,
736        }
737    }
738}
739
740#[cfg(test)]
741#[cfg(not(target_family = "wasm"))] // These tests require tokio::time, which is not implemented on wasm.
742mod tests {
743    use std::{
744        collections::HashMap,
745        ops::Not,
746        sync::{Arc, RwLock},
747        time::Duration,
748    };
749
750    use assert_matches::assert_matches;
751    use matrix_sdk_test_macros::async_test;
752    use strass::assert_let;
753    use tokio::{spawn, task::yield_now, time::sleep};
754
755    use super::{
756        CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
757        CrossProcessLockGuard, CrossProcessLockState, CrossProcessLockUnobtained, TryLock,
758        memory_store_helper::{Lease, try_take_leased_lock},
759    };
760
761    #[derive(Clone, Default)]
762    struct TestStore {
763        leases: Arc<RwLock<HashMap<String, Lease>>>,
764    }
765
766    impl TestStore {
767        fn try_take_leased_lock(
768            &self,
769            lease_duration_ms: u32,
770            key: &str,
771            holder: &str,
772        ) -> Option<CrossProcessLockGeneration> {
773            try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
774        }
775    }
776
777    #[derive(Debug, thiserror::Error)]
778    enum DummyError {}
779
780    impl From<DummyError> for CrossProcessLockError {
781        fn from(value: DummyError) -> Self {
782            Self::TryLock(Arc::new(value))
783        }
784    }
785
786    impl TryLock for TestStore {
787        type LockError = DummyError;
788
789        /// Try to take a lock using the given store.
790        async fn try_lock(
791            &self,
792            lease_duration_ms: u32,
793            key: &str,
794            holder: &str,
795        ) -> Result<Option<CrossProcessLockGeneration>, Self::LockError> {
796            Ok(self.try_take_leased_lock(lease_duration_ms, key, holder))
797        }
798    }
799
800    async fn release_lock(lock: CrossProcessLockGuard) {
801        drop(lock);
802        yield_now().await;
803    }
804
805    type TestResult = Result<(), CrossProcessLockError>;
806    type CrossProcessLock = super::CrossProcessLock<TestStore>;
807
808    #[async_test]
809    async fn test_simple_lock_unlock() -> TestResult {
810        let store = TestStore::default();
811        let lock = CrossProcessLock::new(
812            store,
813            "key".to_owned(),
814            CrossProcessLockConfig::multi_process("first"),
815        );
816
817        // The lock plain works when used with a single holder.
818        let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
819        assert_let!(CrossProcessLockState::Clean(guard) = guard);
820        assert!(lock.is_dirty().not());
821
822        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
823        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
824
825        let guard_clone = guard.clone();
826
827        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 2);
828        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 2);
829        assert_eq!(CrossProcessLockGuard::count_holders(&guard_clone.inner), 2);
830
831        // Dropping a guard decreases the number of holders.
832        drop(guard_clone);
833        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
834        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
835
836        // Releasing works.
837        release_lock(guard).await;
838        assert!(lock.is_dirty().not());
839
840        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
841
842        // Spin locking on the same lock always works, assuming no concurrent
843        // access.
844        let guard = lock.spin_lock(None).await?.expect("spin lock must be obtained successfully");
845        assert_let!(CrossProcessLockState::Clean(guard) = guard);
846        assert!(lock.is_dirty().not());
847
848        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
849
850        // Releasing still works.
851        release_lock(guard).await;
852        assert!(lock.is_dirty().not());
853
854        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
855
856        Ok(())
857    }
858
859    #[async_test]
860    async fn test_self_recovery() -> TestResult {
861        let store = TestStore::default();
862        let lock = CrossProcessLock::new(
863            store.clone(),
864            "key".to_owned(),
865            CrossProcessLockConfig::multi_process("first"),
866        );
867
868        // When a lock is obtained…
869        let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
870        assert_let!(CrossProcessLockState::Clean(guard) = guard);
871        assert!(lock.is_dirty().not());
872        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
873        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
874
875        // But then forgotten…
876        drop(lock);
877
878        // Let's ensure the guard keeps acting as a lock holder even if the lock
879        // has dropped.
880        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
881
882        // Okay, enough fun, time to drop it.
883        release_lock(guard).await;
884
885        // And when rematerializing the lock with the same key/value…
886        let lock = CrossProcessLock::new(
887            store.clone(),
888            "key".to_owned(),
889            CrossProcessLockConfig::multi_process("first"),
890        );
891
892        // We still got it.
893        let guard =
894            lock.try_lock_once().await?.expect("lock (again) must be obtained successfully");
895        assert_let!(CrossProcessLockState::Clean(guard) = guard);
896        assert!(lock.is_dirty().not());
897        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
898        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
899
900        Ok(())
901    }
902
903    #[async_test]
904    async fn test_multiple_holders_same_process() -> TestResult {
905        let store = TestStore::default();
906        let lock = CrossProcessLock::new(
907            store,
908            "key".to_owned(),
909            CrossProcessLockConfig::multi_process("first"),
910        );
911
912        // Taking the lock twice…
913        let guard1 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
914        assert_let!(CrossProcessLockState::Clean(guard1) = guard1);
915
916        let guard2 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
917        assert_let!(CrossProcessLockState::Clean(guard2) = guard2);
918
919        assert!(lock.is_dirty().not());
920        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 2);
921        assert_eq!(CrossProcessLockGuard::count_holders(&guard1.inner), 2);
922        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 2);
923
924        // … means we can release it twice.
925        release_lock(guard1).await;
926        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
927        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 1);
928
929        release_lock(guard2).await;
930        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
931
932        assert!(lock.is_dirty().not());
933
934        Ok(())
935    }
936
937    #[async_test]
938    async fn test_multiple_processes() -> TestResult {
939        let store = TestStore::default();
940        let lock1 = CrossProcessLock::new(
941            store.clone(),
942            "key".to_owned(),
943            CrossProcessLockConfig::multi_process("first"),
944        );
945        let lock2 = CrossProcessLock::new(
946            store,
947            "key".to_owned(),
948            CrossProcessLockConfig::multi_process("second"),
949        );
950
951        // `lock1` acquires the lock.
952        let guard1 = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
953        assert_let!(CrossProcessLockState::Clean(guard1) = guard1);
954        assert!(lock1.is_dirty().not());
955        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 1);
956        assert_eq!(CrossProcessLockGuard::count_holders(&guard1.inner), 1);
957
958        // `lock2` cannot acquire the lock.
959        let err = lock2.try_lock_once().await?.expect_err("lock must NOT be obtained");
960        assert_matches!(err, CrossProcessLockUnobtained::Busy);
961
962        // `lock2` is waiting in a task.
963        let lock2_clone = lock2.clone();
964        let task = spawn(async move { lock2_clone.spin_lock(Some(500)).await });
965
966        yield_now().await;
967
968        release_lock(guard1).await;
969        sleep(Duration::from_millis(super::EXTEND_LEASE_EVERY_MS * 2)).await;
970        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 0);
971
972        // Once `lock1` is released, `lock2` managed to obtain it.
973        let guard2 = task
974            .await
975            .expect("join handle is properly awaited")
976            .expect("lock is successfully attempted")
977            .expect("lock must be obtained successfully");
978        assert_let!(CrossProcessLockState::Clean(guard2) = guard2);
979
980        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 0);
981        assert_eq!(CrossProcessLock::count_holders(&lock2.inner), 1);
982        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 1);
983
984        // `lock1` and `lock2` are both clean!
985        assert!(lock1.is_dirty().not());
986        assert!(lock2.is_dirty().not());
987
988        // Now if `lock1` tries to obtain the lock with a small timeout, it will
989        // fail.
990        assert_matches!(
991            lock1.spin_lock(Some(200)).await,
992            Ok(Err(CrossProcessLockUnobtained::TimedOut))
993        );
994
995        Ok(())
996    }
997
998    #[async_test]
999    async fn test_multiple_processes_up_to_dirty() -> TestResult {
1000        let store = TestStore::default();
1001        let lock1 = CrossProcessLock::new(
1002            store.clone(),
1003            "key".to_owned(),
1004            CrossProcessLockConfig::multi_process("first"),
1005        );
1006        let lock2 = CrossProcessLock::new(
1007            store,
1008            "key".to_owned(),
1009            CrossProcessLockConfig::multi_process("second"),
1010        );
1011
1012        // Obtain `lock1` once.
1013        {
1014            let guard = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1015            assert_matches!(guard, CrossProcessLockState::Clean(_));
1016            assert!(lock1.is_dirty().not());
1017            drop(guard);
1018
1019            yield_now().await;
1020        }
1021
1022        // Obtain `lock2` once.
1023        {
1024            let guard = lock2.try_lock_once().await?.expect("lock must be obtained successfully");
1025            assert_matches!(guard, CrossProcessLockState::Clean(_));
1026            assert!(lock1.is_dirty().not());
1027            drop(guard);
1028
1029            yield_now().await;
1030        }
1031
1032        for _ in 0..3 {
1033            // Obtain `lock1` once more. Now it's dirty because `lock2` has
1034            // acquired the lock meanwhile.
1035            {
1036                let guard =
1037                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1038                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1039                assert!(lock1.is_dirty());
1040
1041                drop(guard);
1042                yield_now().await;
1043            }
1044
1045            // Obtain `lock1` once more! It still dirty because it has not been
1046            // marked as non-dirty.
1047            {
1048                let guard =
1049                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1050                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1051                assert!(lock1.is_dirty());
1052                lock1.clear_dirty();
1053
1054                drop(guard);
1055                yield_now().await;
1056            }
1057
1058            // Obtain `lock1` once more. Now it's clear!
1059            {
1060                let guard =
1061                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1062                assert_matches!(guard, CrossProcessLockState::Clean(_));
1063                assert!(lock1.is_dirty().not());
1064
1065                drop(guard);
1066                yield_now().await;
1067            }
1068
1069            // Same dance with `lock2`!
1070            {
1071                let guard =
1072                    lock2.try_lock_once().await?.expect("lock must be obtained successfully");
1073                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1074                assert!(lock2.is_dirty());
1075                lock2.clear_dirty();
1076
1077                drop(guard);
1078                yield_now().await;
1079            }
1080        }
1081
1082        Ok(())
1083    }
1084}
1085
1086/// Some code that is shared by almost all `MemoryStore` implementations out
1087/// there.
1088pub mod memory_store_helper {
1089    use std::collections::{HashMap, hash_map::Entry};
1090
1091    use ruma::time::{Duration, Instant};
1092
1093    use super::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION};
1094
1095    #[derive(Debug)]
1096    pub struct Lease {
1097        holder: String,
1098        expiration: Instant,
1099        generation: CrossProcessLockGeneration,
1100    }
1101
1102    pub fn try_take_leased_lock(
1103        leases: &mut HashMap<String, Lease>,
1104        lease_duration_ms: u32,
1105        key: &str,
1106        holder: &str,
1107    ) -> Option<CrossProcessLockGeneration> {
1108        let now = Instant::now();
1109        let expiration = now + Duration::from_millis(lease_duration_ms.into());
1110
1111        match leases.entry(key.to_owned()) {
1112            // There is an existing holder.
1113            Entry::Occupied(mut entry) => {
1114                let Lease {
1115                    holder: current_holder,
1116                    expiration: current_expiration,
1117                    generation: current_generation,
1118                } = entry.get_mut();
1119
1120                if current_holder == holder {
1121                    // We had the lease before, extend it.
1122                    *current_expiration = expiration;
1123
1124                    Some(*current_generation)
1125                } else {
1126                    // We didn't have it.
1127                    if *current_expiration < now {
1128                        // Steal it!
1129                        *current_holder = holder.to_owned();
1130                        *current_expiration = expiration;
1131                        *current_generation += 1;
1132
1133                        Some(*current_generation)
1134                    } else {
1135                        // We tried our best.
1136                        None
1137                    }
1138                }
1139            }
1140
1141            // There is no holder, easy.
1142            Entry::Vacant(entry) => {
1143                entry.insert(Lease {
1144                    holder: holder.to_owned(),
1145                    expiration: Instant::now() + Duration::from_millis(lease_duration_ms.into()),
1146                    generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1147                });
1148
1149                Some(FIRST_CROSS_PROCESS_LOCK_GENERATION)
1150            }
1151        }
1152    }
1153}