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}