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
60/// a 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
66/// acquire the underlying lock of a [`CrossProcessLock`].
67///
68/// For example, this can be useful when writing a function which
69/// is 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
86/// or [`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
112    /// the 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 lock
152        // 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 lock _and_ the
166            // `renew_task` have been dropped. In this case, whether the lock is dirty or
167            // 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
175    /// this method is called. This allows recovering from a dirty state and
176    /// marking 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 _and_ the
179        // `renew_task` have been dropped. Marking the lock as non-dirty makes no
180        // 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
211    /// mustn't be touched.
212    ///
213    /// When the number of holders is decreased to 0, then the lock must be
214    /// released in the store.
215    //
216    // Notes about the `Arc`/`Weak` usage:
217    //
218    // - We want to track the number of holders, i.e. the number of guards. To achieve that, we
219    //   could use a thread-safe counter, or hijack `Arc` and `Weak` which provide two thread-safe
220    //   counters: strong count and weak count.
221    // - `CrossProcessLock` holds an `Arc` (this field).
222    // - `renew_task` holds an `Arc` (a clone of this field).
223    // - `CrossProcessLockGuard` holds a `Weak` (it could use an `Arc`, but a `Weak` is fine in
224    //   this context and offers a unique counter for guards!).
225    // - Counting holders = counting the number of `Weak` pointers.
226    // - It is safe to upgrade the `Weak` pointer to an `Arc` (to get information about dirtiness)
227    //   in a guard because the `renew_task` holds a clone of the `Arc` and will not exit until all
228    //   guards have been dropped.
229    // - It is always possible to create a `Weak` pointer (i) either from `CrossProcessLock` by
230    //   using `Arc::downgrade`, (ii) or from `CrossProcessLockGuard` by cloning it.
231    inner: Arc<CrossProcessLockInner>,
232
233    /// The key used in the key/value mapping for the lock entry.
234    lock_key: String,
235
236    /// A mutex to control an attempt to take the lock, to prevent someone using
237    /// it in a re-entrant way.
238    locking_attempt: Arc<Mutex<()>>,
239
240    /// Backoff time, in milliseconds.
241    backoff: Arc<Mutex<WaitingTime>>,
242
243    /// The cross-process lock configuration.
244    config: CrossProcessLockConfig,
245}
246
247/// Inner data for [`CrossProcessLock`] and [`CrossProcessLockGuard`].
248#[derive(Debug)]
249struct CrossProcessLockInner {
250    /// Current renew task spawned by [`CrossProcessLock::try_lock_once`].
251    ///
252    /// It is not used directly by [`CrossProcessLockGuard`]. It is stored here
253    /// to ensure the task will drop once the lock and all the guards drop.
254    renew_task: Mutex<Option<AbortOnDrop<()>>>,
255
256    /// This lock generation.
257    generation: AtomicU64,
258
259    /// Whether the lock has been dirtied.
260    ///
261    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
262    /// of _dirty_.
263    is_dirty: AtomicBool,
264}
265
266impl CrossProcessLockInner {
267    /// Determine whether the cross-process lock is dirty.
268    ///
269    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
270    /// of _dirty_.
271    pub fn is_dirty(&self) -> bool {
272        self.is_dirty.load(Ordering::SeqCst)
273    }
274
275    /// Clear the dirty state from this cross-process lock.
276    ///
277    /// If the cross-process lock is dirtied, it will remain dirtied until
278    /// this method is called. This allows recovering from a dirty state and
279    /// marking that it has recovered.
280    pub fn clear_dirty(&self) {
281        self.is_dirty.store(false, Ordering::SeqCst);
282    }
283}
284
285/// Amount of time a lease of the lock should last, in milliseconds.
286pub const LEASE_DURATION_MS: u32 = 500;
287
288/// Period of time between two attempts to extend the lease. We'll
289/// re-request a lease for an entire duration of `LEASE_DURATION_MS`
290/// milliseconds, every `EXTEND_LEASE_EVERY_MS`, so this has to
291/// be an amount safely low compared to `LEASE_DURATION_MS`, to make sure
292/// that we can miss a deadline without compromising the lock.
293pub const EXTEND_LEASE_EVERY_MS: u64 = 50;
294
295/// Initial backoff, in milliseconds. This is the time we wait the first
296/// time, if taking the lock initially failed.
297const INITIAL_BACKOFF_MS: u32 = 10;
298
299/// Maximal backoff, in milliseconds. This is the maximum amount of time
300/// we'll wait for the lock, *between two attempts*.
301pub const MAX_BACKOFF_MS: u32 = 1000;
302
303/// Sentinel value representing the absence of a lock generation value.
304///
305/// When the lock is created, it has no generation. Once locked, it receives its
306/// first generation from [`TryLock::try_lock`]. Subsequent lockings may
307/// generate new lock generation. The generation is incremented by 1 every time.
308///
309/// The first generation is defined by [`FIRST_CROSS_PROCESS_LOCK_GENERATION`].
310pub const NO_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 0;
311
312/// Describe the first lock generation value (see
313/// [`CrossProcessLockGeneration`]).
314pub const FIRST_CROSS_PROCESS_LOCK_GENERATION: CrossProcessLockGeneration = 1;
315
316impl<L> CrossProcessLock<L>
317where
318    L: TryLock + Clone + SendOutsideWasm + SyncOutsideWasm + 'static,
319{
320    /// Create a new cross-process lock.
321    ///
322    /// # Parameters
323    ///
324    /// - `lock_key`: key in the key-value store to store the lock's state.
325    /// - `config`: the cross-process lock configuration to use, if it's
326    ///   [`CrossProcessLockConfig::SingleProcess`], no actual lock will be
327    ///   taken.
328    pub fn new(locker: L, lock_key: String, config: CrossProcessLockConfig) -> Self {
329        Self {
330            locker: Arc::new(locker),
331            lock_key,
332            locking_attempt: Arc::new(Mutex::new(())),
333            inner: Arc::new(CrossProcessLockInner {
334                renew_task: Default::default(),
335                generation: AtomicU64::new(NO_CROSS_PROCESS_LOCK_GENERATION),
336
337                is_dirty: AtomicBool::new(false),
338            }),
339            backoff: Arc::new(Mutex::new(WaitingTime::Some(INITIAL_BACKOFF_MS))),
340            config,
341        }
342    }
343
344    /// Count the number of holders.
345    ///
346    /// # Safety
347    ///
348    /// This method by itself is safe, but using it correctly requires extra
349    /// care. Another thread can change the weak count at any time, including
350    /// potentially between calling this method and acting on the result.
351    fn count_holders(inner: &Arc<CrossProcessLockInner>) -> usize {
352        Arc::weak_count(inner)
353    }
354
355    /// Determine whether the cross-process lock is dirty.
356    ///
357    /// See [`CrossProcessLockState::Dirty`] to learn more about the semantics
358    /// of _dirty_.
359    pub fn is_dirty(&self) -> bool {
360        self.inner.is_dirty()
361    }
362
363    /// Clear the dirty state from this cross-process lock.
364    ///
365    /// If the cross-process lock is dirtied, it will remain dirtied until
366    /// this method is called. This allows recovering from a dirty state and
367    /// marking that it has recovered.
368    pub fn clear_dirty(&self) {
369        self.inner.clear_dirty();
370    }
371
372    /// Try to lock once, returns whether the lock was obtained or not.
373    ///
374    /// The lock can be obtained but it can be dirty. In all cases, the renew
375    /// task will run in the background.
376    #[instrument(skip(self), fields(?self.lock_key, ?self.config, ?self.inner.generation))]
377    pub async fn try_lock_once(&self) -> AcquireCrossProcessLockResult<L::LockError> {
378        // If it's not `MultiProcess`, this behaves as a no-op
379        let CrossProcessLockConfig::MultiProcess { holder_name } = &self.config else {
380            let guard = CrossProcessLockGuard::new(&self.inner);
381            return Ok(Ok(CrossProcessLockState::Clean(guard)));
382        };
383
384        // Hold onto the locking attempt mutex for the entire lifetime of this
385        // function, to avoid multiple reentrant calls.
386        let mut _attempt = self.locking_attempt.lock().await;
387
388        // If there is at least one other holder, it means the lock has already been
389        // acquired, and we can safely generate a new guard.
390        if Self::count_holders(&self.inner) > 0 {
391            // Note: between the above “count” and the `CrossProcessLockGuard::new` below,
392            // another thread may decrement the number of holders. That's fine because that
393            // means the lock was taken by at least one thread, and after this
394            // call it will be taken by at least one thread.
395            //
396            // Because `locking_attempt` is acquired, the task cannot drop the lock while
397            // 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 time the lock is
408                // 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 lock is
438        // acquired, or because all previous guards have been dropped). We're going to
439        // 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        // - either the task was done,
445        // - or it was still running, but taking a lock in the database has to be an
446        //   atomic operation running in a transaction.
447        drop(renew_task.take());
448
449        // Restart a new one.
450        *renew_task = Some(
451            spawn({
452                let locker = self.locker.clone();
453                let lock_key = self.lock_key.clone();
454                let locking_attempt = self.locking_attempt.clone();
455                let config = self.config.clone();
456
457                // By cloning `CrossProcessLockInner`, we ensure the task acts as a lock holder.
458                let inner = self.inner.clone();
459
460                async move {
461                    let CrossProcessLockConfig::MultiProcess { holder_name } = config else {
462                        return;
463                    };
464
465                    loop {
466                        {
467                            // First, check if there are still users of this lock.
468                            //
469                            // This is not racy, because:
470                            // - the `locking_attempt` mutex makes sure we don't have unexpected
471                            //   interactions with the non-atomic sequence above in `try_lock_once`,
472                            // - other holders will only decrease over time.
473
474                            let _guard = locking_attempt.lock().await;
475
476                            // There are no more holders. We can quit.
477                            if Self::count_holders(&inner) == 0 {
478                                trace!("exiting the lease extension loop");
479
480                                // Cancel the lease with another 0ms lease.
481                                // If we don't get the lock, that's (weird but) fine.
482                                let fut = locker.try_lock(0, &lock_key, &holder_name);
483                                let _ = fut.await;
484
485                                // Exit the loop.
486                                break;
487                            }
488                        }
489
490                        sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await;
491
492                        match locker.try_lock(LEASE_DURATION_MS, &lock_key, &holder_name).await {
493                            Ok(Some(_generation)) => {
494                                // It's impossible that the generation can be
495                                // different from the previous generation.
496                                //
497                                // As long as the task runs, the lock is
498                                // renewed, so the generation remains the same.
499                                // If the lock is not taken, it's because the
500                                // lease has expired, which is represented by
501                                // the `Ok(None)` value, and the task must stop.
502                            }
503
504                            Ok(None) => {
505                                error!(
506                                    "Failed to renew the lock lease: the lock could not be obtained"
507                                );
508
509                                // Exit the loop.
510                                break;
511                            }
512
513                            Err(err) => {
514                                error!("Error when extending the lock lease: {err:#}");
515
516                                // Exit the loop.
517                                break;
518                            }
519                        }
520                    }
521                }
522            })
523            .abort_on_drop(),
524        );
525
526        let guard = CrossProcessLockGuard::new(&self.inner);
527
528        Ok(Ok(if self.is_dirty() {
529            CrossProcessLockState::Dirty(guard)
530        } else {
531            CrossProcessLockState::Clean(guard)
532        }))
533    }
534
535    /// Attempt to take the lock, with exponential backoff if the lock has
536    /// already been taken before.
537    ///
538    /// The `max_backoff` parameter is the maximum time (in milliseconds) that
539    /// should be waited for, between two attempts. When that time is
540    /// reached a second time, the lock will stop attempting to get the lock
541    /// and will return a timeout error upon locking. If not provided,
542    /// will wait for [`MAX_BACKOFF_MS`].
543    #[instrument(skip(self), fields(?self.lock_key, ?self.config))]
544    pub async fn spin_lock(
545        &self,
546        max_backoff: Option<u32>,
547    ) -> AcquireCrossProcessLockResult<L::LockError> {
548        // If there is no holder, this behaves as a no-op
549        let max_backoff = max_backoff.unwrap_or(MAX_BACKOFF_MS);
550
551        // Note: reads/writes to the backoff are racy across threads in theory, but the
552        // lock in `try_lock_once` should sequentialize it all.
553
554        loop {
555            // If the cross-process lock config is not `MultiProcess`, this behaves as a
556            // no-op and we just return
557            let lock_result = self.try_lock_once().await?;
558
559            if lock_result.is_ok() {
560                if matches!(self.config, CrossProcessLockConfig::MultiProcess { .. }) {
561                    // Reset backoff before returning, for the next attempt to lock.
562                    *self.backoff.lock().await = WaitingTime::Some(INITIAL_BACKOFF_MS);
563                }
564
565                return Ok(lock_result);
566            }
567
568            // Exponential backoff! Multiply by 2 the time we've waited before, cap it to
569            // max_backoff.
570            let mut backoff = self.backoff.lock().await;
571
572            let wait = match &mut *backoff {
573                WaitingTime::Some(val) => {
574                    let wait = *val;
575                    *val = val.saturating_mul(2);
576                    if *val >= max_backoff {
577                        *backoff = WaitingTime::Stop;
578                    }
579                    wait
580                }
581                WaitingTime::Stop => {
582                    // We've reached the maximum backoff, abandon.
583                    return Ok(Err(CrossProcessLockUnobtained::TimedOut));
584                }
585            };
586
587            debug!("Waiting {wait} before re-attempting to take the lock");
588            sleep(Duration::from_millis(wait.into())).await;
589        }
590    }
591
592    /// Returns the value in the database that represents the holder's
593    /// identifier.
594    pub fn lock_holder(&self) -> Option<&str> {
595        self.config.holder_name()
596    }
597}
598
599/// Represent a successful result of a locking attempt, either by
600/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
601#[derive(Debug)]
602#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
603pub enum CrossProcessLockState {
604    /// The lock has been obtained successfully, all good.
605    Clean(CrossProcessLockGuard),
606
607    /// The lock has been obtained successfully, but the lock is dirty!
608    ///
609    /// This holder has obtained this cross-process lock once, then another
610    /// holder has obtained this cross-process lock _before_ this holder
611    /// obtained it again. The lock is marked as dirty. It means the value
612    /// protected by the cross-process lock may need to be reloaded if
613    /// synchronisation is important.
614    ///
615    /// Until [`CrossProcessLock::clear_dirty`] is called,
616    /// [`CrossProcessLock::is_dirty`], [`CrossProcessLock::try_lock_once`] and
617    /// [`CrossProcessLock::spin_lock`] will report the lock as dirty. Put it
618    /// differently: dirty once, dirty forever, unless
619    /// [`CrossProcessLock::clear_dirty`] is called.
620    Dirty(CrossProcessLockGuard),
621}
622
623impl CrossProcessLockState {
624    /// Map this value into the inner [`CrossProcessLockGuard`].
625    pub fn into_guard(self) -> CrossProcessLockGuard {
626        match self {
627            Self::Clean(guard) | Self::Dirty(guard) => guard,
628        }
629    }
630
631    /// Map this [`CrossProcessLockState`] into a
632    /// [`MappedCrossProcessLockState`].
633    ///
634    /// This is helpful when one wants to create its own wrapper over
635    /// [`CrossProcessLockGuard`].
636    pub fn map<F, G>(self, mapper: F) -> MappedCrossProcessLockState<G>
637    where
638        F: FnOnce(CrossProcessLockGuard) -> G,
639    {
640        match self {
641            Self::Clean(guard) => MappedCrossProcessLockState::Clean(mapper(guard)),
642            Self::Dirty(guard) => MappedCrossProcessLockState::Dirty(mapper(guard)),
643        }
644    }
645}
646
647/// A mapped [`CrossProcessLockState`].
648///
649/// Created by [`CrossProcessLockState::map`].
650#[derive(Debug)]
651#[must_use = "If unused, the `CrossProcessLock` will unlock at the end of the lease"]
652pub enum MappedCrossProcessLockState<G> {
653    /// The equivalent of [`CrossProcessLockState::Clean`].
654    Clean(G),
655
656    /// The equivalent of [`CrossProcessLockState::Dirty`].
657    Dirty(G),
658}
659
660impl<G> MappedCrossProcessLockState<G> {
661    /// Return `Some(G)` if `Self` is [`Clean`][Self::Clean].
662    pub fn as_clean(&self) -> Option<&G> {
663        match self {
664            Self::Clean(guard) => Some(guard),
665            Self::Dirty(_) => None,
666        }
667    }
668}
669
670/// Represent an unsuccessful result of a lock attempt, either by
671/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
672#[derive(Clone, Debug, thiserror::Error)]
673pub enum CrossProcessLockUnobtained {
674    /// The lock couldn't be obtained immediately because it is busy, i.e. it is
675    /// held by another holder.
676    #[error(
677        "The lock couldn't be obtained immediately because it is busy, i.e. it is held by another holder"
678    )]
679    Busy,
680
681    /// The lock couldn't be obtained after several attempts: locking has timed
682    /// out.
683    #[error("The lock couldn't be obtained after several attempts: locking has timed out")]
684    TimedOut,
685}
686
687/// Union of [`CrossProcessLockUnobtained`] and [`TryLock::LockError`].
688#[derive(Clone, Debug, thiserror::Error)]
689pub enum CrossProcessLockError {
690    #[error(transparent)]
691    Unobtained(#[from] CrossProcessLockUnobtained),
692
693    #[error(transparent)]
694    #[cfg(not(target_family = "wasm"))]
695    TryLock(#[from] Arc<dyn Error + Send + Sync>),
696
697    #[error(transparent)]
698    #[cfg(target_family = "wasm")]
699    TryLock(#[from] Arc<dyn Error>),
700}
701
702/// The cross-process lock config to use for the various stores.
703#[derive(Clone, Debug)]
704pub enum CrossProcessLockConfig {
705    /// The stores will be used in multiple processes, the holder name for the
706    /// cross-process lock is the associated `String`.
707    MultiProcess {
708        /// The name of the holder of the cross-process lock.
709        holder_name: String,
710    },
711    /// The stores will be used in a single process, there is no need for a
712    /// cross-process lock.
713    SingleProcess,
714}
715
716impl CrossProcessLockConfig {
717    /// Helper for quickly creating a [`CrossProcessLockConfig::MultiProcess`]
718    /// variant.
719    pub fn multi_process(holder_name: impl Into<String>) -> Self {
720        Self::MultiProcess { holder_name: holder_name.into() }
721    }
722
723    /// The holder name for the cross-process lock. This is only relevant for
724    /// [`CrossProcessLockConfig::MultiProcess`] variants.
725    pub fn holder_name(&self) -> Option<&str> {
726        match self {
727            Self::MultiProcess { holder_name } => Some(holder_name),
728            Self::SingleProcess => None,
729        }
730    }
731}
732
733#[cfg(test)]
734#[cfg(not(target_family = "wasm"))] // These tests require tokio::time, which is not implemented on wasm.
735mod tests {
736    use std::{
737        collections::HashMap,
738        ops::Not,
739        sync::{Arc, RwLock},
740        time::Duration,
741    };
742
743    use assert_matches::assert_matches;
744    use assert_matches2::assert_let;
745    use matrix_sdk_test_macros::async_test;
746    use tokio::{spawn, task::yield_now, time::sleep};
747
748    use super::{
749        CrossProcessLockConfig, CrossProcessLockError, CrossProcessLockGeneration,
750        CrossProcessLockGuard, CrossProcessLockState, CrossProcessLockUnobtained, TryLock,
751        memory_store_helper::{Lease, try_take_leased_lock},
752    };
753
754    #[derive(Clone, Default)]
755    struct TestStore {
756        leases: Arc<RwLock<HashMap<String, Lease>>>,
757    }
758
759    impl TestStore {
760        fn try_take_leased_lock(
761            &self,
762            lease_duration_ms: u32,
763            key: &str,
764            holder: &str,
765        ) -> Option<CrossProcessLockGeneration> {
766            try_take_leased_lock(&mut self.leases.write().unwrap(), lease_duration_ms, key, holder)
767        }
768    }
769
770    #[derive(Debug, thiserror::Error)]
771    enum DummyError {}
772
773    impl From<DummyError> for CrossProcessLockError {
774        fn from(value: DummyError) -> Self {
775            Self::TryLock(Arc::new(value))
776        }
777    }
778
779    impl TryLock for TestStore {
780        type LockError = DummyError;
781
782        /// Try to take a lock using the given store.
783        async fn try_lock(
784            &self,
785            lease_duration_ms: u32,
786            key: &str,
787            holder: &str,
788        ) -> Result<Option<CrossProcessLockGeneration>, Self::LockError> {
789            Ok(self.try_take_leased_lock(lease_duration_ms, key, holder))
790        }
791    }
792
793    async fn release_lock(lock: CrossProcessLockGuard) {
794        drop(lock);
795        yield_now().await;
796    }
797
798    type TestResult = Result<(), CrossProcessLockError>;
799    type CrossProcessLock = super::CrossProcessLock<TestStore>;
800
801    #[async_test]
802    async fn test_simple_lock_unlock() -> TestResult {
803        let store = TestStore::default();
804        let lock = CrossProcessLock::new(
805            store,
806            "key".to_owned(),
807            CrossProcessLockConfig::multi_process("first"),
808        );
809
810        // The lock plain works when used with a single holder.
811        let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
812        assert_let!(CrossProcessLockState::Clean(guard) = guard);
813        assert!(lock.is_dirty().not());
814
815        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
816        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
817
818        let guard_clone = guard.clone();
819
820        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 2);
821        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 2);
822        assert_eq!(CrossProcessLockGuard::count_holders(&guard_clone.inner), 2);
823
824        // Dropping a guard decreases the number of holders.
825        drop(guard_clone);
826        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
827        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
828
829        // Releasing works.
830        release_lock(guard).await;
831        assert!(lock.is_dirty().not());
832
833        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
834
835        // Spin locking on the same lock always works, assuming no concurrent access.
836        let guard = lock.spin_lock(None).await?.expect("spin lock must be obtained successfully");
837        assert_let!(CrossProcessLockState::Clean(guard) = guard);
838        assert!(lock.is_dirty().not());
839
840        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
841
842        // Releasing still works.
843        release_lock(guard).await;
844        assert!(lock.is_dirty().not());
845
846        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
847
848        Ok(())
849    }
850
851    #[async_test]
852    async fn test_self_recovery() -> TestResult {
853        let store = TestStore::default();
854        let lock = CrossProcessLock::new(
855            store.clone(),
856            "key".to_owned(),
857            CrossProcessLockConfig::multi_process("first"),
858        );
859
860        // When a lock is obtained…
861        let guard = lock.try_lock_once().await?.expect("lock must be obtained successfully");
862        assert_let!(CrossProcessLockState::Clean(guard) = guard);
863        assert!(lock.is_dirty().not());
864        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
865        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
866
867        // But then forgotten…
868        drop(lock);
869
870        // Let's ensure the guard keeps acting as a lock holder even if the lock has
871        // dropped.
872        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
873
874        // Okay, enough fun, time to drop it.
875        release_lock(guard).await;
876
877        // And when rematerializing the lock with the same key/value…
878        let lock = CrossProcessLock::new(
879            store.clone(),
880            "key".to_owned(),
881            CrossProcessLockConfig::multi_process("first"),
882        );
883
884        // We still got it.
885        let guard =
886            lock.try_lock_once().await?.expect("lock (again) must be obtained successfully");
887        assert_let!(CrossProcessLockState::Clean(guard) = guard);
888        assert!(lock.is_dirty().not());
889        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
890        assert_eq!(CrossProcessLockGuard::count_holders(&guard.inner), 1);
891
892        Ok(())
893    }
894
895    #[async_test]
896    async fn test_multiple_holders_same_process() -> TestResult {
897        let store = TestStore::default();
898        let lock = CrossProcessLock::new(
899            store,
900            "key".to_owned(),
901            CrossProcessLockConfig::multi_process("first"),
902        );
903
904        // Taking the lock twice…
905        let guard1 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
906        assert_let!(CrossProcessLockState::Clean(guard1) = guard1);
907
908        let guard2 = lock.try_lock_once().await?.expect("lock must be obtained successfully");
909        assert_let!(CrossProcessLockState::Clean(guard2) = guard2);
910
911        assert!(lock.is_dirty().not());
912        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 2);
913        assert_eq!(CrossProcessLockGuard::count_holders(&guard1.inner), 2);
914        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 2);
915
916        // … means we can release it twice.
917        release_lock(guard1).await;
918        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 1);
919        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 1);
920
921        release_lock(guard2).await;
922        assert_eq!(CrossProcessLock::count_holders(&lock.inner), 0);
923
924        assert!(lock.is_dirty().not());
925
926        Ok(())
927    }
928
929    #[async_test]
930    async fn test_multiple_processes() -> TestResult {
931        let store = TestStore::default();
932        let lock1 = CrossProcessLock::new(
933            store.clone(),
934            "key".to_owned(),
935            CrossProcessLockConfig::multi_process("first"),
936        );
937        let lock2 = CrossProcessLock::new(
938            store,
939            "key".to_owned(),
940            CrossProcessLockConfig::multi_process("second"),
941        );
942
943        // `lock1` acquires the lock.
944        let guard1 = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
945        assert_let!(CrossProcessLockState::Clean(guard1) = guard1);
946        assert!(lock1.is_dirty().not());
947        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 1);
948        assert_eq!(CrossProcessLockGuard::count_holders(&guard1.inner), 1);
949
950        // `lock2` cannot acquire the lock.
951        let err = lock2.try_lock_once().await?.expect_err("lock must NOT be obtained");
952        assert_matches!(err, CrossProcessLockUnobtained::Busy);
953
954        // `lock2` is waiting in a task.
955        let lock2_clone = lock2.clone();
956        let task = spawn(async move { lock2_clone.spin_lock(Some(500)).await });
957
958        yield_now().await;
959
960        release_lock(guard1).await;
961        sleep(Duration::from_millis(super::EXTEND_LEASE_EVERY_MS * 2)).await;
962        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 0);
963
964        // Once `lock1` is released, `lock2` managed to obtain it.
965        let guard2 = task
966            .await
967            .expect("join handle is properly awaited")
968            .expect("lock is successfully attempted")
969            .expect("lock must be obtained successfully");
970        assert_let!(CrossProcessLockState::Clean(guard2) = guard2);
971
972        assert_eq!(CrossProcessLock::count_holders(&lock1.inner), 0);
973        assert_eq!(CrossProcessLock::count_holders(&lock2.inner), 1);
974        assert_eq!(CrossProcessLockGuard::count_holders(&guard2.inner), 1);
975
976        // `lock1` and `lock2` are both clean!
977        assert!(lock1.is_dirty().not());
978        assert!(lock2.is_dirty().not());
979
980        // Now if `lock1` tries to obtain the lock with a small timeout, it will fail.
981        assert_matches!(
982            lock1.spin_lock(Some(200)).await,
983            Ok(Err(CrossProcessLockUnobtained::TimedOut))
984        );
985
986        Ok(())
987    }
988
989    #[async_test]
990    async fn test_multiple_processes_up_to_dirty() -> TestResult {
991        let store = TestStore::default();
992        let lock1 = CrossProcessLock::new(
993            store.clone(),
994            "key".to_owned(),
995            CrossProcessLockConfig::multi_process("first"),
996        );
997        let lock2 = CrossProcessLock::new(
998            store,
999            "key".to_owned(),
1000            CrossProcessLockConfig::multi_process("second"),
1001        );
1002
1003        // Obtain `lock1` once.
1004        {
1005            let guard = lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1006            assert_matches!(guard, CrossProcessLockState::Clean(_));
1007            assert!(lock1.is_dirty().not());
1008            drop(guard);
1009
1010            yield_now().await;
1011        }
1012
1013        // Obtain `lock2` once.
1014        {
1015            let guard = lock2.try_lock_once().await?.expect("lock must be obtained successfully");
1016            assert_matches!(guard, CrossProcessLockState::Clean(_));
1017            assert!(lock1.is_dirty().not());
1018            drop(guard);
1019
1020            yield_now().await;
1021        }
1022
1023        for _ in 0..3 {
1024            // Obtain `lock1` once more. Now it's dirty because `lock2` has acquired the
1025            // lock meanwhile.
1026            {
1027                let guard =
1028                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1029                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1030                assert!(lock1.is_dirty());
1031
1032                drop(guard);
1033                yield_now().await;
1034            }
1035
1036            // Obtain `lock1` once more! It still dirty because it has not been marked as
1037            // non-dirty.
1038            {
1039                let guard =
1040                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1041                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1042                assert!(lock1.is_dirty());
1043                lock1.clear_dirty();
1044
1045                drop(guard);
1046                yield_now().await;
1047            }
1048
1049            // Obtain `lock1` once more. Now it's clear!
1050            {
1051                let guard =
1052                    lock1.try_lock_once().await?.expect("lock must be obtained successfully");
1053                assert_matches!(guard, CrossProcessLockState::Clean(_));
1054                assert!(lock1.is_dirty().not());
1055
1056                drop(guard);
1057                yield_now().await;
1058            }
1059
1060            // Same dance with `lock2`!
1061            {
1062                let guard =
1063                    lock2.try_lock_once().await?.expect("lock must be obtained successfully");
1064                assert_matches!(guard, CrossProcessLockState::Dirty(_));
1065                assert!(lock2.is_dirty());
1066                lock2.clear_dirty();
1067
1068                drop(guard);
1069                yield_now().await;
1070            }
1071        }
1072
1073        Ok(())
1074    }
1075}
1076
1077/// Some code that is shared by almost all `MemoryStore` implementations out
1078/// there.
1079pub mod memory_store_helper {
1080    use std::collections::{HashMap, hash_map::Entry};
1081
1082    use ruma::time::{Duration, Instant};
1083
1084    use super::{CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION};
1085
1086    #[derive(Debug)]
1087    pub struct Lease {
1088        holder: String,
1089        expiration: Instant,
1090        generation: CrossProcessLockGeneration,
1091    }
1092
1093    pub fn try_take_leased_lock(
1094        leases: &mut HashMap<String, Lease>,
1095        lease_duration_ms: u32,
1096        key: &str,
1097        holder: &str,
1098    ) -> Option<CrossProcessLockGeneration> {
1099        let now = Instant::now();
1100        let expiration = now + Duration::from_millis(lease_duration_ms.into());
1101
1102        match leases.entry(key.to_owned()) {
1103            // There is an existing holder.
1104            Entry::Occupied(mut entry) => {
1105                let Lease {
1106                    holder: current_holder,
1107                    expiration: current_expiration,
1108                    generation: current_generation,
1109                } = entry.get_mut();
1110
1111                if current_holder == holder {
1112                    // We had the lease before, extend it.
1113                    *current_expiration = expiration;
1114
1115                    Some(*current_generation)
1116                } else {
1117                    // We didn't have it.
1118                    if *current_expiration < now {
1119                        // Steal it!
1120                        *current_holder = holder.to_owned();
1121                        *current_expiration = expiration;
1122                        *current_generation += 1;
1123
1124                        Some(*current_generation)
1125                    } else {
1126                        // We tried our best.
1127                        None
1128                    }
1129                }
1130            }
1131
1132            // There is no holder, easy.
1133            Entry::Vacant(entry) => {
1134                entry.insert(Lease {
1135                    holder: holder.to_owned(),
1136                    expiration: Instant::now() + Duration::from_millis(lease_duration_ms.into()),
1137                    generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1138                });
1139
1140                Some(FIRST_CROSS_PROCESS_LOCK_GENERATION)
1141            }
1142        }
1143    }
1144}