use crate::error::CanoError;
use crate::resource::Resources;
use crate::task::{TaskConfig, TaskResult};
use std::borrow::Cow;
use std::fmt;
use std::hash::Hash;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TimerOutcome {
Duration(Duration),
Until(Instant),
}
#[crate::task::timer]
pub trait TimerTask<TState, TResourceKey = Cow<'static, str>>: Send + Sync
where
TState: Clone + fmt::Debug + Send + Sync + 'static,
TResourceKey: Hash + Eq + Send + Sync + 'static,
{
fn config(&self) -> TaskConfig {
crate::task::minimal_task_config()
}
fn name(&self) -> Cow<'static, str> {
crate::task::default_task_name::<Self>()
}
async fn wait(&self, res: &Resources<TResourceKey>) -> Result<TimerOutcome, CanoError>;
async fn after_wait(
&self,
res: &Resources<TResourceKey>,
) -> Result<TaskResult<TState>, CanoError>;
}
pub async fn run_timer<T, S, K>(t: &T, res: &Resources<K>) -> Result<TaskResult<S>, CanoError>
where
T: TimerTask<S, K> + ?Sized,
S: Clone + fmt::Debug + Send + Sync + 'static,
K: Hash + Eq + Send + Sync + 'static,
{
match t.wait(res).await? {
TimerOutcome::Duration(d) => tokio::time::sleep(d).await,
TimerOutcome::Until(i) => tokio::time::sleep_until(tokio::time::Instant::from_std(i)).await,
}
t.after_wait(res).await
}
pub type DynTimerTask<TState, TResourceKey = Cow<'static, str>> =
dyn TimerTask<TState, TResourceKey> + Send + Sync;
pub type TimerTaskObject<TState, TResourceKey = Cow<'static, str>> =
std::sync::Arc<DynTimerTask<TState, TResourceKey>>;
#[cfg(test)]
mod tests {
use super::*;
use crate::resource::Resources;
use crate::task;
use crate::task::Task;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Step {
Wait,
Done,
Next,
}
struct DurationTimer;
#[task::timer]
impl TimerTask<Step> for DurationTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn test_timer_task_duration_via_run_timer() {
let timer = DurationTimer;
let res = Resources::new();
let result = run_timer(&timer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
}
#[tokio::test]
async fn test_timer_task_duration_via_task_run() {
let timer = DurationTimer;
let res = Resources::new();
let result = Task::run(&timer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
}
struct PastInstantTimer;
#[task::timer]
impl TimerTask<Step> for PastInstantTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
let past = Instant::now() - Duration::from_secs(60);
Ok(TimerOutcome::Until(past))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn test_timer_task_until_past_instant_fires_immediately() {
let timer = PastInstantTimer;
let res = Resources::new();
let start = Instant::now();
let result = run_timer(&timer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
assert!(
start.elapsed() < Duration::from_millis(500),
"past Until should not sleep"
);
}
struct SplitTimer;
#[task::timer]
impl TimerTask<Step> for SplitTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Split(vec![Step::Wait, Step::Next]))
}
}
#[tokio::test]
async fn test_timer_task_split() {
let timer = SplitTimer;
let res = Resources::new();
let result = Task::run(&timer, &res).await.unwrap();
assert_eq!(result, TaskResult::Split(vec![Step::Wait, Step::Next]));
}
struct CustomTimer;
#[task::timer]
impl TimerTask<Step> for CustomTimer {
fn name(&self) -> Cow<'static, str> {
Cow::Borrowed("my-custom-timer")
}
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[test]
fn test_timer_task_default_config_is_minimal() {
let timer = DurationTimer;
assert_eq!(
TimerTask::<Step>::config(&timer).retry_mode.max_attempts(),
1,
"TimerTask default config must be minimal (no retries)"
);
}
#[test]
fn test_timer_task_default_name_contains_type_name() {
let timer = DurationTimer;
let name = TimerTask::<Step>::name(&timer);
assert!(
name.contains("DurationTimer"),
"default name should contain the type name, got: {name}",
);
}
#[test]
fn test_timer_task_name_override_forwarded_to_task() {
let timer = CustomTimer;
assert_eq!(TimerTask::<Step>::name(&timer), "my-custom-timer");
assert_eq!(Task::name(&timer), "my-custom-timer");
}
#[tokio::test]
async fn test_timer_task_as_dyn_task() {
let timer: Arc<dyn Task<Step>> = Arc::new(DurationTimer);
let res = Resources::new();
let result = Task::run(timer.as_ref(), &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
}
struct WaitErrorTimer;
#[task::timer]
impl TimerTask<Step> for WaitErrorTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Err(CanoError::task_execution("wait failed"))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Ok(TaskResult::Single(Step::Done))
}
}
#[tokio::test]
async fn test_wait_error_propagates() {
let timer = WaitErrorTimer;
let res = Resources::new();
let err = Task::run(&timer, &res).await.unwrap_err();
assert!(matches!(err, CanoError::TaskExecution(_)));
}
struct AfterWaitErrorTimer;
#[task::timer]
impl TimerTask<Step> for AfterWaitErrorTimer {
async fn wait(&self, _res: &Resources) -> Result<TimerOutcome, CanoError> {
Ok(TimerOutcome::Duration(Duration::ZERO))
}
async fn after_wait(&self, _res: &Resources) -> Result<TaskResult<Step>, CanoError> {
Err(CanoError::task_execution("after_wait failed"))
}
}
#[tokio::test]
async fn test_after_wait_error_propagates() {
let timer = AfterWaitErrorTimer;
let res = Resources::new();
let err = Task::run(&timer, &res).await.unwrap_err();
assert!(matches!(err, CanoError::TaskExecution(_)));
}
#[tokio::test]
async fn test_run_timer_dyn_dispatch() {
let timer: &dyn TimerTask<Step> = &DurationTimer;
let res = Resources::new();
let result = run_timer(timer, &res).await.unwrap();
assert_eq!(result, TaskResult::Single(Step::Done));
}
}