Skip to content

Commit 78180d8

Browse files
[runtime] Introduce Runtime-Agnostic, Asynchronous RwLock (#836)
1 parent d89a19b commit 78180d8

5 files changed

Lines changed: 153 additions & 2 deletions

File tree

Cargo.lock

Lines changed: 48 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ opentelemetry-otlp = "0.28.0"
6767
opentelemetry_sdk = "0.28.0"
6868
tracing-opentelemetry = "0.29.0"
6969
rayon = "1.10.0"
70+
async-lock = "3.4.0"
7071

7172
[profile.bench]
7273
# Because we enable overflow checks in "release," we should benchmark with them.

runtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ tracing-subscriber = { workspace = true, features = ["fmt", "json", "env-filter"
2626
opentelemetry = { workspace = true }
2727
tracing-opentelemetry = { workspace = true }
2828
rayon = { workspace = true }
29+
async-lock = { workspace = true }
2930

3031
# Enable "js" feature when WASM is target
3132
[target.'cfg(target_arch = "wasm32")'.dependencies.getrandom]

runtime/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ cfg_if::cfg_if! {
3737
mod storage;
3838
pub mod telemetry;
3939
mod utils;
40-
pub use utils::{create_pool, reschedule, Handle, Signal, Signaler};
40+
pub use utils::{
41+
create_pool, reschedule, Handle, RwLock, RwLockReadGuard, RwLockWriteGuard, Signal, Signaler,
42+
};
4143

4244
/// Prefix for runtime metrics.
4345
const METRICS_PREFIX: &str = "runtime";

runtime/src/utils.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,83 @@ pub fn create_pool<S: Spawner + Metrics>(
346346
.build()
347347
}
348348

349+
/// Async reader–writer lock.
350+
///
351+
/// Powered by [async_lock::RwLock], `RwLock` provides both fair writer acquisition
352+
/// and `try_read` / `try_write` without waiting (without any runtime-specific dependencies).
353+
///
354+
/// Usage:
355+
/// ```rust
356+
/// use commonware_runtime::{Spawner, Runner, Signaler, deterministic, RwLock};
357+
///
358+
/// let executor = deterministic::Runner::default();
359+
/// executor.start(|context| async move {
360+
/// // Create a new RwLock
361+
/// let lock = RwLock::new(2);
362+
///
363+
/// // many concurrent readers
364+
/// let r1 = lock.read().await;
365+
/// let r2 = lock.read().await;
366+
/// assert_eq!(*r1 + *r2, 4);
367+
///
368+
/// // exclusive writer
369+
/// drop((r1, r2));
370+
/// let mut w = lock.write().await;
371+
/// *w += 1;
372+
/// });
373+
/// ```
374+
pub struct RwLock<T>(async_lock::RwLock<T>);
375+
376+
/// Shared guard returned by [`RwLock::read`].
377+
pub type RwLockReadGuard<'a, T> = async_lock::RwLockReadGuard<'a, T>;
378+
379+
/// Exclusive guard returned by [`RwLock::write`].
380+
pub type RwLockWriteGuard<'a, T> = async_lock::RwLockWriteGuard<'a, T>;
381+
382+
impl<T> RwLock<T> {
383+
/// Create a new lock.
384+
#[inline]
385+
pub const fn new(value: T) -> Self {
386+
Self(async_lock::RwLock::new(value))
387+
}
388+
389+
/// Acquire a shared read guard.
390+
#[inline]
391+
pub async fn read(&self) -> RwLockReadGuard<'_, T> {
392+
self.0.read().await
393+
}
394+
395+
/// Acquire an exclusive write guard.
396+
#[inline]
397+
pub async fn write(&self) -> RwLockWriteGuard<'_, T> {
398+
self.0.write().await
399+
}
400+
401+
/// Try to get a read guard without waiting.
402+
#[inline]
403+
pub fn try_read(&self) -> Option<RwLockReadGuard<'_, T>> {
404+
self.0.try_read()
405+
}
406+
407+
/// Try to get a write guard without waiting.
408+
#[inline]
409+
pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, T>> {
410+
self.0.try_write()
411+
}
412+
413+
/// Get mutable access without locking (requires `&mut self`).
414+
#[inline]
415+
pub fn get_mut(&mut self) -> &mut T {
416+
self.0.get_mut()
417+
}
418+
419+
/// Consume the lock, returning the inner value.
420+
#[inline]
421+
pub fn into_inner(self) -> T {
422+
self.0.into_inner()
423+
}
424+
}
425+
349426
#[cfg(test)]
350427
async fn task(i: usize) -> usize {
351428
for _ in 0..5 {
@@ -376,7 +453,7 @@ pub fn run_tasks(tasks: usize, runner: crate::deterministic::Runner) -> (String,
376453
#[cfg(test)]
377454
mod tests {
378455
use super::*;
379-
use crate::{tokio, Metrics};
456+
use crate::{deterministic, tokio, Metrics};
380457
use commonware_macros::test_traced;
381458
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
382459

@@ -396,4 +473,26 @@ mod tests {
396473
});
397474
});
398475
}
476+
477+
#[test_traced]
478+
fn test_rwlock() {
479+
let executor = deterministic::Runner::default();
480+
executor.start(|_| async move {
481+
// Create a new RwLock
482+
let lock = RwLock::new(100);
483+
484+
// many concurrent readers
485+
let r1 = lock.read().await;
486+
let r2 = lock.read().await;
487+
assert_eq!(*r1 + *r2, 200);
488+
489+
// exclusive writer
490+
drop((r1, r2)); // all readers must go away
491+
let mut w = lock.write().await;
492+
*w += 1;
493+
494+
// Check the value
495+
assert_eq!(*w, 101);
496+
});
497+
}
399498
}

0 commit comments

Comments
 (0)