use std::{
future::Future,
time::{Duration, SystemTime},
};
use crate::{
error::RuntimeError,
runtime::{CancellationToken, RunOptions},
};
const BOUND_POLL_INTERVAL: Duration = Duration::from_millis(25);
#[derive(Debug, Clone, Default)]
pub struct CompactionBounds {
pub cancellation: Option<CancellationToken>,
pub deadline: Option<SystemTime>,
}
impl CompactionBounds {
pub fn from_run_options(options: &RunOptions) -> Self {
Self {
cancellation: options.cancellation.clone(),
deadline: options.deadline,
}
}
pub fn is_bounded(&self) -> bool {
self.cancellation.is_some() || self.deadline.is_some()
}
pub fn check(&self) -> Result<(), RuntimeError> {
if self
.cancellation
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
return Err(RuntimeError::Cancelled);
}
if self
.deadline
.is_some_and(|deadline| SystemTime::now() >= deadline)
{
return Err(RuntimeError::DeadlineExceeded);
}
Ok(())
}
pub async fn guard<F>(&self, future: F) -> Result<F::Output, RuntimeError>
where
F: Future,
{
self.check()?;
if !self.is_bounded() {
return Ok(future.await);
}
tokio::pin!(future);
loop {
tokio::select! {
biased;
output = &mut future => return Ok(output),
() = tokio::time::sleep(self.poll_delay()) => self.check()?,
}
}
}
pub(crate) async fn sleep(&self, duration: Duration) -> Result<(), RuntimeError> {
self.guard(tokio::time::sleep(duration)).await
}
fn poll_delay(&self) -> Duration {
match self.deadline {
Some(deadline) => deadline
.duration_since(SystemTime::now())
.unwrap_or(Duration::ZERO)
.min(BOUND_POLL_INTERVAL),
None => BOUND_POLL_INTERVAL,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_bounds_bind_nothing() {
let bounds = CompactionBounds::default();
assert!(!bounds.is_bounded());
assert!(bounds.check().is_ok());
}
#[test]
fn run_bounds_carry_cancellation_and_deadline_but_not_a_graceful_stop() {
let cancellation = CancellationToken::default();
let stop = CancellationToken::default();
let deadline = SystemTime::now() + Duration::from_secs(30);
let options = RunOptions {
cancellation: Some(cancellation.clone()),
stop: Some(stop.clone()),
deadline: Some(deadline),
..RunOptions::default()
};
let bounds = CompactionBounds::from_run_options(&options);
assert_eq!(bounds.deadline, Some(deadline));
assert!(bounds.check().is_ok());
stop.cancel();
assert!(
bounds.check().is_ok(),
"a graceful stop ends a run at a boundary; it does not abandon work in flight"
);
cancellation.cancel();
assert!(matches!(bounds.check(), Err(RuntimeError::Cancelled)));
}
#[tokio::test(start_paused = true)]
async fn a_guarded_future_is_abandoned_when_the_token_trips() {
let cancellation = CancellationToken::default();
let bounds = CompactionBounds {
cancellation: Some(cancellation.clone()),
deadline: None,
};
let canceller = tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(40)).await;
cancellation.cancel();
});
let started = tokio::time::Instant::now();
let guarded: Result<(), RuntimeError> = bounds.guard(std::future::pending()).await;
canceller.await.expect("canceller task");
assert!(matches!(guarded, Err(RuntimeError::Cancelled)));
assert!(
started.elapsed() < Duration::from_millis(100),
"the guard notices the cancel within a poll interval or two of it"
);
}
#[tokio::test]
async fn a_guarded_future_is_never_polled_past_the_deadline() {
let bounds = CompactionBounds {
cancellation: None,
deadline: Some(SystemTime::now() - Duration::from_secs(1)),
};
let mut polled = false;
let guarded = bounds
.guard(async {
polled = true;
})
.await;
assert!(matches!(guarded, Err(RuntimeError::DeadlineExceeded)));
assert!(!polled, "an expired bound must not start the work");
}
#[tokio::test(start_paused = true)]
async fn a_guarded_sleep_ends_early_on_cancellation() {
let cancellation = CancellationToken::default();
let bounds = CompactionBounds {
cancellation: Some(cancellation.clone()),
deadline: None,
};
cancellation.cancel();
let started = tokio::time::Instant::now();
let slept = bounds.sleep(Duration::from_secs(30)).await;
assert!(matches!(slept, Err(RuntimeError::Cancelled)));
assert_eq!(
started.elapsed(),
Duration::ZERO,
"an already-cancelled sleep waits for nothing at all"
);
}
}