Skip to content

Commit

Permalink
Control if redis uses async-std or tokio via feature flags (#4)
Browse files Browse the repository at this point in the history
* Control if redis uses async-std or tokio via feature flags

* Do not include LockGuard and dependent functions if tokio-comp is enabled

* make tests pass with cargo test --features tokio-comp

* add doc comments; promote comments to doc comments; alter cfg conditions

* only compile the lock module if either of the async runtime flags are present

* test raii unlocking only when only async-std is enabled

* add default-features test
  • Loading branch information
hgzimmerman authored Aug 30, 2023
1 parent 6d9e1ca commit 4de6748
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 3 deletions.
18 changes: 17 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,24 @@ jobs:
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
# https://twitter.com/jonhoo/status/1571290371124260865
- name: cargo test --locked
- name: cargo test --locked --all-features --all-targets
run: cargo test --locked --all-features --all-targets
# Not all tests run with the --all-features flag.
# This job is to ensure that tests that pertain only to the default feature set are run.
default-features:
runs-on: ubuntu-latest
name: ubuntu / stable / default-features
steps:
- uses: actions/checkout@v3
with:
submodules: true
- name: Install stable
uses: dtolnay/rust-toolchain@stable
- name: cargo generate-lockfile
if: hashFiles('Cargo.lock') == ''
run: cargo generate-lockfile
- name: cargo test --locked
run: cargo test --locked --all-targets
minimal:
runs-on: ubuntu-latest
name: ubuntu / stable / minimal-versions
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ license = "BSD-3-Clause"
readme = "README.md"
edition = "2021"

[features]
async-std-comp = ["redis/async-std-comp"]
tokio-comp = ["redis/tokio-comp"]
default = ["async-std-comp"]

[dependencies]
redis = { version = "0.21.6", features = ["async-std-comp"] }
redis = { version = "0.21.6" }
tokio = { version = "1.27.0", features = ["rt", "time"] }
rand = "0.8.5"
futures = "0.3.24"
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[cfg(any(feature = "async-std-comp", feature = "tokio-comp"))]
mod lock;

#[cfg(any(feature = "async-std-comp", feature = "tokio-comp"))]
pub use crate::lock::{Lock, LockError, LockGuard, LockManager};
62 changes: 61 additions & 1 deletion src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,24 @@ pub struct Lock<'a> {
pub lock_manager: &'a LockManager,
}

/// Upon dropping the guard, `LockManager::unlock` will be ran synchronously on the executor.
///
/// This is known to block the tokio runtime if this happens inside of the context of a tokio runtime
/// if `tokio-comp` is enabled as a feature on this crate or the `redis` crate.
///
/// To eliminate this risk, if the `tokio-comp` flag is enabled, the `Drop` impl will not be compiled,
/// meaning that dropping the `LockGuard` will be a no-op.
/// Under this circumstance, `LockManager::unlock` can be called manually using the inner `lock` at the appropriate
/// point to release the lock taken in `Redis`.
#[derive(Debug, Clone)]
pub struct LockGuard<'a> {
pub lock: Lock<'a>,
}

/// Dropping this guard inside the context of a tokio runtime if `tokio-comp` is enabled
/// will block the tokio runtime.
/// Because of this, the guard is not compiled if `tokio-comp` is enabled.
#[cfg(not(feature = "tokio-comp"))]
impl Drop for LockGuard<'_> {
fn drop(&mut self) {
futures::executor::block_on(self.lock.lock_manager.unlock(&self.lock));
Expand Down Expand Up @@ -253,10 +266,23 @@ impl LockManager {
.await
}

/// Loops until the lock is acquired.
///
/// The lock is placed in a guard that will unlock the lock when the guard is dropped.
#[cfg(feature = "async-std-comp")]
pub async fn acquire<'a>(&'a self, resource: &'a [u8], ttl: usize) -> LockGuard<'a> {
let lock = self.acquire_no_guard(resource, ttl).await;
LockGuard { lock }
}

/// Loops until the lock is acquired.
///
/// Either lock's value must expire after the ttl has elapsed,
/// or `LockManager::unlock` must be called to allow other clients to lock the same resource.
pub async fn acquire_no_guard<'a>(&'a self, resource: &'a [u8], ttl: usize) -> Lock<'a> {
loop {
if let Ok(lock) = self.lock(resource, ttl).await {
return LockGuard { lock };
return lock;
}
}
}
Expand Down Expand Up @@ -476,6 +502,7 @@ mod tests {
Ok(())
}

#[cfg(all(not(feature = "tokio-comp"), feature = "async-std-comp"))]
#[tokio::test]
async fn test_lock_lock_unlock_raii() -> Result<()> {
let (_containers, addresses) = create_clients();
Expand Down Expand Up @@ -507,6 +534,38 @@ mod tests {
Ok(())
}

#[cfg(feature = "tokio-comp")]
#[tokio::test]
async fn test_lock_lock_raii_does_not_unlock_with_tokio_enabled() -> Result<()> {
let (_containers, addresses) = create_clients();

let rl = LockManager::new(addresses.clone());
let rl2 = LockManager::new(addresses.clone());
let key = rl.get_unique_lock_id()?;

async {
let lock_guard = rl.acquire(&key, 1000).await;
let lock = &lock_guard.lock;
assert!(
lock.validity_time > 900,
"validity time: {}",
lock.validity_time
);

if let Ok(_l) = rl2.lock(&key, 1000).await {
panic!("Lock acquired, even though it should be locked")
}
}
.await;

if let Ok(_) = rl2.lock(&key, 1000).await {
panic!("Lock couldn't be acquired");
}

Ok(())
}

#[cfg(feature = "async-std-comp")]
#[tokio::test]
async fn test_lock_extend_lock() -> Result<()> {
let (_containers, addresses) = create_clients();
Expand Down Expand Up @@ -541,6 +600,7 @@ mod tests {
Ok(())
}

#[cfg(feature = "async-std-comp")]
#[tokio::test]
async fn test_lock_extend_lock_releases() -> Result<()> {
let (_containers, addresses) = create_clients();
Expand Down

0 comments on commit 4de6748

Please sign in to comment.