Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: add unit tests for the CryptoStoreLock #2167

Merged
merged 1 commit into from
Jun 29, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 148 additions & 3 deletions crates/matrix-sdk-crypto/src/store/locks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,15 +193,15 @@ impl CryptoStoreLock {
// threads trying to acquire/release the lock at the same time.
let mut holders = self.num_holders.lock().await;

assert!(*holders > 0);
if *holders > 1 {
// There's at least one other holder, so just decrease the number of holders.
*holders -= 1;
trace!("not releasing, because another thread holds onto it");
return Ok(());
}

// Here, holders == 1.
// Here, holders == 1 (or 0, but that is supposed to trigger the
// `MissingLockValue` error then).

let read = self
.store
Expand All @@ -215,7 +215,7 @@ impl CryptoStoreLock {

let removed = self.store.remove_custom_value(&self.lock_key).await?;
if removed {
*holders -= 1;
*holders = 0;
trace!("successfully released");
Ok(())
} else {
Expand All @@ -240,3 +240,148 @@ pub enum LockStoreError {
#[error("a lock timed out")]
LockTimeout,
}

#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use matrix_sdk_test::async_test;
use tokio::spawn;

use super::*;
use crate::store::{IntoCryptoStore as _, MemoryStore};

#[async_test]
async fn test_simple_lock_unlock() -> Result<(), CryptoStoreError> {
let mem_store = MemoryStore::new();
let dyn_store = mem_store.into_crypto_store();

let lock = CryptoStoreLock::new(dyn_store, "key".to_owned(), "first".to_owned());

// The lock plain works when used with a single holder.
let acquired = lock.try_lock_once().await?;
assert!(acquired);
assert_eq!(*lock.num_holders.lock().await, 1);

// Releasing works.
assert!(lock.unlock().await.is_ok());
assert_eq!(*lock.num_holders.lock().await, 0);

// Releasing another time is an error.
assert_matches!(
lock.unlock().await,
Err(CryptoStoreError::Lock(LockStoreError::MissingLockValue))
);

// Spin locking on the same lock always works, assuming no concurrent access.
assert!(lock.spin_lock(None).await.is_ok());

// Releasing works again.
assert!(lock.unlock().await.is_ok());

// Releasing another time is still an error.
assert_matches!(
lock.unlock().await,
Err(CryptoStoreError::Lock(LockStoreError::MissingLockValue))
);

Ok(())
}

#[async_test]
async fn test_self_recovery() -> Result<(), CryptoStoreError> {
let mem_store = MemoryStore::new();
let dyn_store = mem_store.into_crypto_store();

let lock = CryptoStoreLock::new(dyn_store.clone(), "key".to_owned(), "first".to_owned());

// When a lock is acquired...
let acquired = lock.try_lock_once().await?;
assert!(acquired);
assert_eq!(*lock.num_holders.lock().await, 1);

// But then forgotten...
drop(lock);

// The DB still knows about it...
let prev = dyn_store.get_custom_value("key").await?;
assert_eq!(String::from_utf8_lossy(prev.as_ref().unwrap()), "first");

// And when rematerializing the lock with the same key/value...
let lock = CryptoStoreLock::new(dyn_store.clone(), "key".to_owned(), "first".to_owned());

// We still got it.
let acquired = lock.try_lock_once().await?;
assert!(acquired);
assert_eq!(*lock.num_holders.lock().await, 1);

// And can release it.
assert!(lock.unlock().await.is_ok());

Ok(())
}

#[async_test]
async fn test_multiple_holders_same_process() -> Result<(), CryptoStoreError> {
let mem_store = MemoryStore::new();
let dyn_store = mem_store.into_crypto_store();

let lock = CryptoStoreLock::new(dyn_store, "key".to_owned(), "first".to_owned());

// Taking the lock twice...
let acquired = lock.try_lock_once().await?;
assert!(acquired);

let acquired = lock.try_lock_once().await?;
assert!(acquired);

assert_eq!(*lock.num_holders.lock().await, 2);

// ...means we can release it twice.
assert!(lock.unlock().await.is_ok());

assert!(lock.unlock().await.is_ok());

// Releasing another time is still an error.
assert_matches!(
lock.unlock().await,
Err(CryptoStoreError::Lock(LockStoreError::MissingLockValue))
);

Ok(())
}

#[async_test]
async fn test_multiple_processes() -> Result<(), CryptoStoreError> {
let mem_store = MemoryStore::new();
let dyn_store = mem_store.into_crypto_store();

let lock1 = CryptoStoreLock::new(dyn_store.clone(), "key".to_owned(), "first".to_owned());
let lock2 = CryptoStoreLock::new(dyn_store, "key".to_owned(), "second".to_owned());

// When the first process takes the lock...
let acquired1 = lock1.try_lock_once().await?;
assert!(acquired1);

// The second can't take it immediately.
let acquired2 = lock2.try_lock_once().await?;
assert!(!acquired2);

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

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

lock1.unlock().await?;

// lock2 in the background managed to get the lock.
assert!(handle.await.is_ok());

// Now if lock1 tries to get the lock with a small timeout, it will fail.
assert_matches!(
lock1.spin_lock(Some(200)).await,
Err(CryptoStoreError::Lock(LockStoreError::LockTimeout))
);

Ok(())
}
}
Loading