@@ -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 ) ]
478497pub 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 {
499513mod 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