@@ -311,3 +311,104 @@ impl BackingStore for LockableEventCacheStore {
311
311
} )
312
312
}
313
313
}
314
+
315
+ #[ cfg( all( test, not( target_arch = "wasm32" ) ) ) ] // because time is a thing
316
+ mod tests {
317
+ use std:: { sync:: Arc , time:: Duration } ;
318
+
319
+ use matrix_sdk_common:: store_locks:: MAX_BACKOFF_MS ;
320
+ use matrix_sdk_test:: async_test;
321
+ use ruma:: user_id;
322
+ use tokio:: time:: sleep;
323
+
324
+ use super :: MemoryStore ;
325
+ use crate :: { store:: StoreConfig , test_utils:: logged_in_base_client_with_store_config} ;
326
+
327
+ #[ async_test]
328
+ async fn test_not_poisoned_lock ( ) {
329
+ let client = logged_in_base_client_with_store_config (
330
+ Some ( user_id ! ( "@client:sdk.rust" ) ) ,
331
+ StoreConfig :: new ( "holderA" . to_string ( ) ) ,
332
+ )
333
+ . await ;
334
+
335
+ let event_cache_store_lock = client. event_cache_store ( ) ;
336
+
337
+ // `lock_unchecked` is okay.
338
+ let guard = event_cache_store_lock. lock_unchecked ( ) . await ;
339
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired and may or may not be poisoned
340
+
341
+ // `lock` is okay.
342
+ let guard = event_cache_store_lock. lock ( ) . await ;
343
+
344
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired
345
+ assert ! ( guard. unwrap( ) . is_ok( ) ) ; // lock is not poisoned
346
+ }
347
+
348
+ #[ async_test]
349
+ async fn test_poisoned_lock ( ) {
350
+ // Use the same memory store between clients A and B.
351
+ let memory_store = Arc :: new ( MemoryStore :: new ( ) ) ;
352
+
353
+ let client_a = logged_in_base_client_with_store_config (
354
+ Some ( user_id ! ( "@client_a:sdk.rust" ) ) ,
355
+ StoreConfig :: new ( "holderA" . to_string ( ) ) . event_cache_store ( memory_store. clone ( ) ) ,
356
+ )
357
+ . await ;
358
+
359
+ let client_b = logged_in_base_client_with_store_config (
360
+ Some ( user_id ! ( "@client_b:sdk.rust" ) ) ,
361
+ StoreConfig :: new ( "holderB" . to_string ( ) ) . event_cache_store ( memory_store) ,
362
+ )
363
+ . await ;
364
+
365
+ let event_cache_store_lock_a = client_a. event_cache_store ( ) ;
366
+ let event_cache_store_lock_b = client_b. event_cache_store ( ) ;
367
+
368
+ // Client A can take the lock because no one has taken it so far.
369
+ {
370
+ // `lock` is okay.
371
+ let guard = event_cache_store_lock_a. lock ( ) . await ;
372
+
373
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired
374
+ assert ! ( guard. unwrap( ) . is_ok( ) ) ; // lock is not poisoned
375
+ }
376
+
377
+ sleep ( Duration :: from_millis ( MAX_BACKOFF_MS as u64 + 100 ) ) . await ;
378
+
379
+ // Client B can take the lock since all locks from A are expired, but
380
+ // now, B is poisoned because the content of the event cache might have
381
+ // been modified (if someone takes a lock, it's probably for a good
382
+ // reason, right?).
383
+ {
384
+ // `lock` is okay.
385
+ let guard = event_cache_store_lock_b. lock ( ) . await ;
386
+
387
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired
388
+ assert ! ( guard. unwrap( ) . is_err( ) ) ; // lock is poisoned!
389
+ }
390
+
391
+ sleep ( Duration :: from_millis ( MAX_BACKOFF_MS as u64 + 100 ) ) . await ;
392
+
393
+ // Client A can take the lock since all locks from B are expired, but
394
+ // now, A is poisoned. Let's not test `lock` but `lock_unchecked` this
395
+ // time.
396
+ {
397
+ // `lock_unchecked` is okay.
398
+ let guard = event_cache_store_lock_a. lock_unchecked ( ) . await ;
399
+
400
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired and might be
401
+ // poisoned, we don't know with this method
402
+ }
403
+
404
+ // Client A can still take the lock because it is holding it. The lock
405
+ // is no more poisoned.
406
+ {
407
+ // `lock` is okay.
408
+ let guard = event_cache_store_lock_a. lock ( ) . await ;
409
+
410
+ assert ! ( guard. is_ok( ) ) ; // lock has been acquired
411
+ assert ! ( guard. unwrap( ) . is_ok( ) ) ; // lock is not poisoned
412
+ }
413
+ }
414
+ }
0 commit comments