use std::sync::Arc;
use std::time::{Duration, Instant};
use dev_report::{CheckResult, Evidence, Severity};
use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
pub async fn try_mutex_lock_with_timeout<'a, T>(
name: impl Into<String>,
lock: &'a Arc<Mutex<T>>,
timeout: Duration,
) -> Result<(CheckResult, MutexGuard<'a, T>), CheckResult> {
let name = name.into();
let started = Instant::now();
match tokio::time::timeout(timeout, lock.lock()).await {
Ok(guard) => {
let elapsed = started.elapsed();
let mut c = CheckResult::pass(format!("async::lock::{name}"))
.with_duration_ms(elapsed.as_millis() as u64);
c.tags = vec!["async".to_string(), "lock".to_string()];
c.evidence = vec![
Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
];
Ok((c, guard))
}
Err(_) => {
let mut c = CheckResult::fail(format!("async::lock::{name}"), Severity::Error)
.with_detail(format!("could not acquire lock within {timeout:?}"));
c.tags = vec![
"async".to_string(),
"lock".to_string(),
"deadlock_suspected".to_string(),
"regression".to_string(),
];
c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
Err(c)
}
}
}
pub async fn try_rwlock_read_with_timeout<'a, T>(
name: impl Into<String>,
lock: &'a Arc<RwLock<T>>,
timeout: Duration,
) -> Result<(CheckResult, RwLockReadGuard<'a, T>), CheckResult> {
let name = name.into();
let started = Instant::now();
match tokio::time::timeout(timeout, lock.read()).await {
Ok(guard) => {
let elapsed = started.elapsed();
let mut c = CheckResult::pass(format!("async::lock::{name}::read"))
.with_duration_ms(elapsed.as_millis() as u64);
c.tags = vec!["async".to_string(), "lock".to_string()];
c.evidence = vec![
Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
];
Ok((c, guard))
}
Err(_) => {
let mut c = CheckResult::fail(format!("async::lock::{name}::read"), Severity::Error)
.with_detail(format!("could not acquire read lock within {timeout:?}"));
c.tags = vec![
"async".to_string(),
"lock".to_string(),
"deadlock_suspected".to_string(),
"regression".to_string(),
];
c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
Err(c)
}
}
}
pub async fn try_rwlock_write_with_timeout<'a, T>(
name: impl Into<String>,
lock: &'a Arc<RwLock<T>>,
timeout: Duration,
) -> Result<(CheckResult, RwLockWriteGuard<'a, T>), CheckResult> {
let name = name.into();
let started = Instant::now();
match tokio::time::timeout(timeout, lock.write()).await {
Ok(guard) => {
let elapsed = started.elapsed();
let mut c = CheckResult::pass(format!("async::lock::{name}::write"))
.with_duration_ms(elapsed.as_millis() as u64);
c.tags = vec!["async".to_string(), "lock".to_string()];
c.evidence = vec![
Evidence::numeric("elapsed_ms", elapsed.as_millis() as f64),
Evidence::numeric("timeout_ms", timeout.as_millis() as f64),
];
Ok((c, guard))
}
Err(_) => {
let mut c = CheckResult::fail(format!("async::lock::{name}::write"), Severity::Error)
.with_detail(format!("could not acquire write lock within {timeout:?}"));
c.tags = vec![
"async".to_string(),
"lock".to_string(),
"deadlock_suspected".to_string(),
"regression".to_string(),
];
c.evidence = vec![Evidence::numeric("timeout_ms", timeout.as_millis() as f64)];
Err(c)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use dev_report::Verdict;
#[tokio::test]
async fn mutex_lock_acquires_under_timeout() {
let m = Arc::new(Mutex::new(0));
let (check, _g) = try_mutex_lock_with_timeout("a", &m, Duration::from_millis(50))
.await
.unwrap();
assert_eq!(check.verdict, Verdict::Pass);
assert!(check.has_tag("lock"));
}
#[tokio::test]
async fn mutex_lock_times_out_when_held() {
let m = Arc::new(Mutex::new(0));
let _held = m.lock().await;
let err = try_mutex_lock_with_timeout("a", &m, Duration::from_millis(20))
.await
.unwrap_err();
assert_eq!(err.verdict, Verdict::Fail);
assert!(err.has_tag("deadlock_suspected"));
assert!(err.has_tag("regression"));
}
#[tokio::test]
async fn rwlock_read_under_timeout() {
let l = Arc::new(RwLock::new(0));
let (check, _g) = try_rwlock_read_with_timeout("a", &l, Duration::from_millis(50))
.await
.unwrap();
assert_eq!(check.verdict, Verdict::Pass);
}
#[tokio::test]
async fn rwlock_write_times_out_when_held() {
let l = Arc::new(RwLock::new(0));
let _held = l.write().await;
let err = try_rwlock_write_with_timeout("a", &l, Duration::from_millis(20))
.await
.unwrap_err();
assert_eq!(err.verdict, Verdict::Fail);
}
}