I wrote module that provides cancelable sleep using Condvar. I want to know if there is any room for improvements(coding style, idiomatic rust, deadlock possibility, ...).
use std::{
sync::{Arc, Condvar, Mutex},
time::{Duration, Instant},
};
struct Inner {
is_canceled: Arc<(Mutex<bool>, Condvar)>,
wakeup_time: Instant,
}
pub fn sleep(duration: Duration) -> (SleepHandle, CancelHandle) {
let inner = Inner {
is_canceled: Arc::new((Mutex::new(false), Condvar::new())),
wakeup_time: Instant::now() + duration,
};
(
SleepHandle::from_inner(&inner),
CancelHandle::from_inner(&inner),
)
}
pub struct SleepHandle(Inner);
impl SleepHandle {
pub fn sleep(self) {
let (lock, cvar) = &*self.0.is_canceled;
let mut is_canceled = lock.lock().unwrap();
while !*is_canceled {
let duration_left = self.0.wakeup_time.checked_duration_since(Instant::now());
match duration_left {
Some(duration_left) => {
let result = cvar
.wait_timeout_while(is_canceled, duration_left, |&mut is_canceled| {
!is_canceled
})
.unwrap();
is_canceled = result.0;
}
None => break,
}
}
}
fn from_inner(inner: &Inner) -> Self {
Self(Inner {
is_canceled: Arc::clone(&inner.is_canceled),
wakeup_time: inner.wakeup_time,
})
}
}
pub struct CancelHandle(Inner);
impl CancelHandle {
pub fn cancel(self) {
let (lock, cvar) = &*self.0.is_canceled;
let mut is_canceled = lock.lock().unwrap();
*is_canceled = true;
cvar.notify_all();
}
fn from_inner(inner: &Inner) -> Self {
Self(Inner {
is_canceled: Arc::clone(&inner.is_canceled),
wakeup_time: inner.wakeup_time,
})
}
}
impl Clone for CancelHandle {
fn clone(&self) -> Self {
CancelHandle::from_inner(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{thread, time::Instant};
#[test]
fn test_sleep() {
let duration = Duration::from_millis(1300);
let (sleep, _) = sleep(duration);
let before = Instant::now();
thread::spawn(move || {
sleep.sleep();
})
.join()
.unwrap();
assert!(before.elapsed().as_millis().abs_diff(duration.as_millis()) < 50);
}
#[test]
fn test_cancel() {
let duration = Duration::from_millis(8000);
let cancel_after = Duration::from_millis(3421);
let (sleep, cancel) = sleep(duration);
let before = Instant::now();
thread::spawn(move || {
let before = Instant::now();
std::thread::sleep(cancel_after);
cancel.cancel();
//assert that cancel does not block
assert!(
before
.elapsed()
.as_millis()
.abs_diff(cancel_after.as_millis())
< 50
);
});
thread::spawn(move || {
sleep.sleep();
})
.join()
.unwrap();
assert!(
before
.elapsed()
.as_millis()
.abs_diff(cancel_after.as_millis())
< 50
);
}
#[test]
fn test_multiple_cancel() {
let duration = Duration::from_millis(8000);
let cancel_after = Duration::from_millis(3421);
let (sleep, cancel) = sleep(duration);
let before = Instant::now();
{
let cancel = cancel.clone();
thread::spawn(move || {
let before = Instant::now();
std::thread::sleep(cancel_after);
cancel.cancel();
//assert that cancel does not block
assert!(
before
.elapsed()
.as_millis()
.abs_diff(cancel_after.as_millis())
< 50
);
});
}
thread::spawn(move || {
let cancel_after = Duration::from_millis(5400);
let before = Instant::now();
std::thread::sleep(cancel_after);
cancel.cancel();
//assert that second cancel does not block
assert!(
before
.elapsed()
.as_millis()
.abs_diff(cancel_after.as_millis())
< 50
);
});
thread::spawn(move || {
sleep.sleep();
})
.join()
.unwrap();
assert!(
before
.elapsed()
.as_millis()
.abs_diff(cancel_after.as_millis())
< 50
);
}
#[test]
fn test_sleep_after_cancel() {
let before = Instant::now();
let (sleep, cancel) = sleep(Duration::from_millis(5000));
cancel.cancel();
sleep.sleep();
assert!(before.elapsed().as_millis() < 50);
}
}