Skip to content

Commit 891db14

Browse files
committed
continue!
1 parent 021a9d6 commit 891db14

File tree

1 file changed

+93
-78
lines changed

1 file changed

+93
-78
lines changed

crates/matrix-sdk-common/src/cross_process_lock.rs

Lines changed: 93 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -238,21 +238,15 @@ where
238238
}
239239

240240
/// Try to lock once, returns whether the lock was obtained or not.
241+
///
242+
/// The lock can be obtained but it can be dirty. In all cases, the renew
243+
/// task will run in the background.
241244
#[instrument(skip(self), fields(?self.lock_key, ?self.lock_holder))]
242-
pub async fn try_lock_once(
243-
&self,
244-
) -> Result<Option<CrossProcessLockGuard>, CrossProcessLockError> {
245+
pub async fn try_lock_once(&self) -> Result<CrossProcessLockResult, CrossProcessLockError> {
245246
// Hold onto the locking attempt mutex for the entire lifetime of this
246247
// function, to avoid multiple reentrant calls.
247248
let mut _attempt = self.locking_attempt.lock().await;
248249

249-
// The lock is already dirtied? Let's stop here.
250-
if self.is_dirty() {
251-
return Err(CrossProcessLockError::LockDirtied(CrossProcessLockGuard::new(
252-
self.num_holders.clone(),
253-
)));
254-
}
255-
256250
// If another thread obtained the lock, make sure to only superficially increase
257251
// the number of holders, and carry on.
258252
if self.num_holders.load(Ordering::SeqCst) > 0 {
@@ -261,9 +255,12 @@ where
261255
// was taken by at least one thread, and after this call it will be
262256
// taken by at least one thread.
263257
trace!("We already had the lock, incrementing holder count");
258+
264259
self.num_holders.fetch_add(1, Ordering::SeqCst);
265-
let guard = CrossProcessLockGuard { num_holders: self.num_holders.clone() };
266-
return Ok(Some(guard));
260+
261+
return Ok(CrossProcessLockResult::Clean(CrossProcessLockGuard::new(
262+
self.num_holders.clone(),
263+
)));
267264
}
268265

269266
if let Some(new_generation) = self
@@ -287,10 +284,6 @@ where
287284
"The lock has been acquired, but it's been dirtied!"
288285
);
289286
self.is_dirty.store(true, Ordering::SeqCst);
290-
291-
return Err(CrossProcessLockError::LockDirtied(CrossProcessLockGuard::new(
292-
self.num_holders.clone(),
293-
)));
294287
}
295288

296289
// This was the same generation, no problem.
@@ -302,7 +295,7 @@ where
302295
trace!("Lock acquired!");
303296
} else {
304297
trace!("Couldn't acquire the lock immediately.");
305-
return Ok(None);
298+
return Ok(CrossProcessLockResult::Unobtained);
306299
}
307300

308301
trace!("Acquired the lock, spawning the lease extension task.");
@@ -357,42 +350,22 @@ where
357350
}
358351
}
359352

360-
// The lock has been dirtied. Exit the loop.
361-
if this.is_dirty() {
362-
break;
363-
}
364-
365353
sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await;
366354

367355
match this
368356
.locker
369357
.try_lock(LEASE_DURATION_MS, &this.lock_key, &this.lock_holder)
370358
.await
371359
{
372-
Ok(Some(new_generation)) => {
373-
match this.lock_generation.swap(new_generation, Ordering::SeqCst) {
374-
// It's impossible to renew the lock if the lock wasn't acquired at
375-
// least once. This is unreachable.
376-
NO_CROSS_PROCESS_LOCK_GENERATION => unreachable!(
377-
"It's impossible to renew a lock lease that has not been acquired once"
378-
),
379-
380-
// This was NOT the same generation, the lock has been dirtied!
381-
previous_generation if previous_generation != new_generation => {
382-
warn!(
383-
?previous_generation,
384-
?new_generation,
385-
"The lock lease has been renewed, but it's been dirtied!"
386-
);
387-
this.is_dirty.store(true, Ordering::SeqCst);
388-
389-
// Exit the loop.
390-
break;
391-
}
392-
393-
// This was the same generation, no problem.
394-
_ => {}
395-
}
360+
Ok(Some(_generation)) => {
361+
// It's impossible that the generation can be different
362+
// from the previous generation.
363+
//
364+
// As long as the task runs, the lock is renewed, so the
365+
// generation remains the same. If the lock is not
366+
// taken, it's because the lease has expired, which is
367+
// represented by the `Ok(None)` value, and the task
368+
// must stop.
396369
}
397370

398371
Ok(None) => {
@@ -414,7 +387,13 @@ where
414387

415388
self.num_holders.fetch_add(1, Ordering::SeqCst);
416389

417-
Ok(Some(CrossProcessLockGuard::new(self.num_holders.clone())))
390+
let guard = CrossProcessLockGuard::new(self.num_holders.clone());
391+
392+
Ok(if self.is_dirty() {
393+
CrossProcessLockResult::Dirty(guard)
394+
} else {
395+
CrossProcessLockResult::Clean(guard)
396+
})
418397
}
419398

420399
/// Attempt to take the lock, with exponential backoff if the lock has
@@ -436,7 +415,7 @@ where
436415
// lock in `try_lock_once` should sequentialize it all.
437416

438417
loop {
439-
if let Some(guard) = self.try_lock_once().await? {
418+
if let Some(guard) = self.try_lock_once().await?.ok() {
440419
// Reset backoff before returning, for the next attempt to lock.
441420
*self.backoff.lock().await = WaitingTime::Some(INITIAL_BACKOFF_MS);
442421
return Ok(guard);
@@ -473,18 +452,53 @@ where
473452
}
474453
}
475454

455+
/// Represent the result of a locking attempt, either by
456+
/// [`CrossProcessLock::try_lock_once`] or [`CrossProcessLock::spin_lock`].
457+
#[derive(Debug)]
458+
pub enum CrossProcessLockResult {
459+
/// The lock has been obtained successfully, all good.
460+
Clean(CrossProcessLockGuard),
461+
462+
/// The lock has been obtained successfully, but the lock is dirty!
463+
///
464+
/// This holder has obtained this cross-process lock once, then another
465+
/// holder has obtained this cross-process lock _before_ this holder
466+
/// obtained it again. The lock is marked as dirty. It means the value
467+
/// protected by the cross-process lock may need to be reloaded if
468+
/// synchronisation is important.
469+
Dirty(CrossProcessLockGuard),
470+
471+
/// The lock has not been obtained.
472+
Unobtained,
473+
}
474+
475+
impl CrossProcessLockResult {
476+
pub fn is_dirty(&self) -> bool {
477+
matches!(self, Self::Dirty(_))
478+
}
479+
480+
/// Convert from [`CrossProcessLockResult`] to
481+
/// [`Option<T>`] where `T` is [`CrossProcessLockGuard`].
482+
pub fn ok(self) -> Option<CrossProcessLockGuard> {
483+
match self {
484+
Self::Clean(guard) | Self::Dirty(guard) => Some(guard),
485+
Self::Unobtained => None,
486+
}
487+
}
488+
489+
/// Return `true` if the lock has been obtained, `false` otherwise.
490+
pub fn is_ok(&self) -> bool {
491+
matches!(self, Self::Clean(_) | Self::Dirty(_))
492+
}
493+
}
494+
476495
/// Error related to the locking API of the store.
477496
#[derive(Debug, thiserror::Error)]
478497
pub enum CrossProcessLockError {
479498
/// Spent too long waiting for a database lock.
480499
#[error("a lock timed out")]
481500
LockTimeout,
482501

483-
/// The lock has been dirtied, i.e. it means another process took the lock
484-
/// while this process was holding it.
485-
#[error("a lock has been dirtied")]
486-
LockDirtied(CrossProcessLockGuard),
487-
488502
#[error(transparent)]
489503
#[cfg(not(target_family = "wasm"))]
490504
TryLockError(#[from] Box<dyn Error + Send + Sync>),
@@ -499,6 +513,7 @@ pub enum CrossProcessLockError {
499513
mod tests {
500514
use std::{
501515
collections::HashMap,
516+
ops::Not,
502517
sync::{Arc, RwLock, atomic},
503518
};
504519

@@ -510,8 +525,8 @@ mod tests {
510525
};
511526

512527
use super::{
513-
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration, CrossProcessLockGuard,
514-
EXTEND_LEASE_EVERY_MS, TryLock,
528+
CrossProcessLock, CrossProcessLockError, CrossProcessLockGeneration,
529+
CrossProcessLockResult, EXTEND_LEASE_EVERY_MS, TryLock,
515530
memory_store_helper::{Lease, try_take_leased_lock},
516531
};
517532

@@ -548,8 +563,8 @@ mod tests {
548563
}
549564
}
550565

551-
async fn release_lock(guard: Option<CrossProcessLockGuard>) {
552-
drop(guard);
566+
async fn release_lock(result: CrossProcessLockResult) {
567+
drop(result);
553568
sleep(Duration::from_millis(EXTEND_LEASE_EVERY_MS)).await;
554569
}
555570

@@ -561,19 +576,19 @@ mod tests {
561576
let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned());
562577

563578
// The lock plain works when used with a single holder.
564-
let acquired = lock.try_lock_once().await?;
565-
assert!(acquired.is_some());
579+
let result = lock.try_lock_once().await?;
580+
assert!(result.is_ok());
566581
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
567582

568583
// Releasing works.
569-
release_lock(acquired).await;
584+
release_lock(result).await;
570585
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
571586

572587
// Spin locking on the same lock always works, assuming no concurrent access.
573-
let acquired = lock.spin_lock(None).await.unwrap();
588+
let guard = lock.spin_lock(None).await.unwrap();
574589

575590
// Releasing still works.
576-
release_lock(Some(acquired)).await;
591+
release_lock(CrossProcessLockResult::Clean(guard)).await;
577592
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
578593

579594
Ok(())
@@ -585,8 +600,8 @@ mod tests {
585600
let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
586601

587602
// When a lock is acquired...
588-
let acquired = lock.try_lock_once().await?;
589-
assert!(acquired.is_some());
603+
let result = lock.try_lock_once().await?;
604+
assert!(result.is_ok());
590605
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
591606

592607
// But then forgotten... (note: no need to release the guard)
@@ -596,8 +611,8 @@ mod tests {
596611
let lock = CrossProcessLock::new(store.clone(), "key".to_owned(), "first".to_owned());
597612

598613
// We still got it.
599-
let acquired = lock.try_lock_once().await?;
600-
assert!(acquired.is_some());
614+
let result = lock.try_lock_once().await?;
615+
assert!(result.is_ok());
601616
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
602617

603618
Ok(())
@@ -609,19 +624,19 @@ mod tests {
609624
let lock = CrossProcessLock::new(store, "key".to_owned(), "first".to_owned());
610625

611626
// Taking the lock twice...
612-
let acquired = lock.try_lock_once().await?;
613-
assert!(acquired.is_some());
627+
let result1 = lock.try_lock_once().await?;
628+
assert!(result1.is_ok());
614629

615-
let acquired2 = lock.try_lock_once().await?;
616-
assert!(acquired2.is_some());
630+
let result2 = lock.try_lock_once().await?;
631+
assert!(result2.is_ok());
617632

618633
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 2);
619634

620635
// ...means we can release it twice.
621-
release_lock(acquired).await;
636+
release_lock(result1).await;
622637
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 1);
623638

624-
release_lock(acquired2).await;
639+
release_lock(result2).await;
625640
assert_eq!(lock.num_holders.load(atomic::Ordering::SeqCst), 0);
626641

627642
Ok(())
@@ -634,22 +649,22 @@ mod tests {
634649
let lock2 = CrossProcessLock::new(store, "key".to_owned(), "second".to_owned());
635650

636651
// When the first process takes the lock...
637-
let acquired1 = lock1.try_lock_once().await?;
638-
assert!(acquired1.is_some());
652+
let result1 = lock1.try_lock_once().await?;
653+
assert!(result1.is_ok());
639654

640655
// The second can't take it immediately.
641-
let acquired2 = lock2.try_lock_once().await?;
642-
assert!(acquired2.is_none());
656+
let result2 = lock2.try_lock_once().await?;
657+
assert!(result2.is_ok().not());
643658

644659
let lock2_clone = lock2.clone();
645660
let handle = spawn(async move { lock2_clone.spin_lock(Some(1000)).await });
646661

647662
sleep(Duration::from_millis(100)).await;
648663

649-
drop(acquired1);
664+
drop(result1);
650665

651666
// lock2 in the background manages to get the lock at some point.
652-
let _acquired2 = handle
667+
let _result2 = handle
653668
.await
654669
.expect("join handle is properly awaited")
655670
.expect("lock was obtained after spin-locking");

0 commit comments

Comments
 (0)