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