use aion_core::{Event, RunId, TimerCancelCause, TimerId};
use chrono::Utc;
use uuid::Uuid;
use crate::durability::{DurabilityError, Recorder};
pub const DEADLINE_TIMER_PREFIX: &str = "deadline:";
pub const WORKFLOW_TIMEOUT_DESCRIPTOR: &str = "workflow";
pub fn deadline_timer_id(run_id: &RunId) -> Result<TimerId, aion_core::IdError> {
TimerId::named(format!("{DEADLINE_TIMER_PREFIX}{run_id}"))
}
#[must_use]
pub fn is_deadline_timer(timer_id: &TimerId) -> bool {
timer_id
.name()
.is_some_and(|name| name.starts_with(DEADLINE_TIMER_PREFIX))
}
#[must_use]
pub fn deadline_run_id(timer_id: &TimerId) -> Option<RunId> {
let suffix = timer_id.name()?.strip_prefix(DEADLINE_TIMER_PREFIX)?;
Uuid::parse_str(suffix).ok().map(RunId::new)
}
#[must_use]
pub fn outstanding_deadline_timer(history: &[Event], run_id: &RunId) -> Option<TimerId> {
let mut live: Option<TimerId> = None;
for event in history {
let (timer_id, started) = match event {
Event::TimerStarted { timer_id, .. } => (timer_id, true),
Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
(timer_id, false)
}
_ => continue,
};
if !is_deadline_timer(timer_id) || deadline_run_id(timer_id).as_ref() != Some(run_id) {
continue;
}
live = if started {
Some(timer_id.clone())
} else {
None
};
}
live
}
pub async fn retire_run_deadline(
recorder: &mut Recorder,
history: &[Event],
run_id: &RunId,
) -> Result<(), DurabilityError> {
if let Some(deadline_id) = outstanding_deadline_timer(history, run_id) {
recorder
.record_timer_cancelled(Utc::now(), deadline_id, TimerCancelCause::WorkflowIntent)
.await?;
}
Ok(())
}
#[derive(thiserror::Error, Debug)]
#[error("deadline handler failed: {0}")]
pub struct DeadlineHandlerError(pub String);
#[async_trait::async_trait]
pub trait DeadlineHandler: Send + Sync {
async fn on_deadline_elapsed(
&self,
workflow_id: aion_core::WorkflowId,
run_id: RunId,
) -> Result<(), DeadlineHandlerError>;
}
#[cfg(test)]
mod tests {
use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
use uuid::Uuid;
use super::{
deadline_run_id, deadline_timer_id, is_deadline_timer, outstanding_deadline_timer,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn envelope(seq: u64, workflow_id: &WorkflowId) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: chrono::Utc::now(),
workflow_id: workflow_id.clone(),
}
}
#[test]
fn outstanding_deadline_timer_is_none_for_a_run_without_a_deadline() -> TestResult {
let workflow_id = WorkflowId::new_v4();
let run_id = RunId::new_v4();
let other_run = RunId::new_v4();
let history = vec![
Event::TimerStarted {
envelope: envelope(1, &workflow_id),
timer_id: deadline_timer_id(&other_run)?,
fire_at: chrono::Utc::now(),
},
Event::TimerStarted {
envelope: envelope(2, &workflow_id),
timer_id: TimerId::anonymous(3),
fire_at: chrono::Utc::now(),
},
];
assert_eq!(outstanding_deadline_timer(&history, &run_id), None);
Ok(())
}
#[test]
fn outstanding_deadline_timer_tracks_liveness_and_scopes_to_the_run() -> TestResult {
let workflow_id = WorkflowId::new_v4();
let this_run = RunId::new_v4();
let other_run = RunId::new_v4();
let this_deadline = deadline_timer_id(&this_run)?;
let other_deadline = deadline_timer_id(&other_run)?;
let armed = vec![
Event::TimerStarted {
envelope: envelope(1, &workflow_id),
timer_id: this_deadline.clone(),
fire_at: chrono::Utc::now(),
},
Event::TimerStarted {
envelope: envelope(2, &workflow_id),
timer_id: other_deadline,
fire_at: chrono::Utc::now(),
},
];
assert_eq!(
outstanding_deadline_timer(&armed, &this_run),
Some(this_deadline.clone())
);
let mut cancelled = armed;
cancelled.push(Event::TimerCancelled {
envelope: envelope(3, &workflow_id),
timer_id: this_deadline,
cause: TimerCancelCause::WorkflowIntent,
});
assert_eq!(outstanding_deadline_timer(&cancelled, &this_run), None);
Ok(())
}
#[test]
fn deadline_timer_id_round_trips_to_its_run() -> TestResult {
let run_id = RunId::new(Uuid::from_u128(42));
let timer_id = deadline_timer_id(&run_id)?;
assert!(is_deadline_timer(&timer_id));
assert_eq!(deadline_run_id(&timer_id), Some(run_id));
Ok(())
}
#[test]
fn author_named_timer_is_not_a_deadline() -> TestResult {
let timer_id = TimerId::named("review-deadline")?;
assert!(!is_deadline_timer(&timer_id));
assert_eq!(deadline_run_id(&timer_id), None);
Ok(())
}
#[test]
fn anonymous_timer_is_not_a_deadline() {
let timer_id = TimerId::anonymous(7);
assert!(!is_deadline_timer(&timer_id));
assert_eq!(deadline_run_id(&timer_id), None);
}
#[test]
fn deadline_prefixed_but_malformed_suffix_has_no_run() -> TestResult {
let timer_id = TimerId::named("deadline:not-a-uuid")?;
assert!(is_deadline_timer(&timer_id));
assert_eq!(deadline_run_id(&timer_id), None);
Ok(())
}
}