use std::collections::{BTreeMap, BTreeSet};
use aion_core::{ActivityId, Event, RunId};
pub(crate) type AttemptAxis = (u64, u32);
#[derive(Debug, Default)]
pub(crate) struct RunScope {
by_run: BTreeMap<String, BTreeSet<AttemptAxis>>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum RunScopeError {
#[error(
"run {run_id} is not a run of workflow {workflow_id}; \
call describe_run to obtain a real run_id rather than constructing one"
)]
UnknownRun {
workflow_id: String,
run_id: String,
},
#[error(
"run {run_id} never dispatched activity {activity_id} attempt {attempt}; \
another run of workflow {workflow_id} may have, and its transcript is not this run's"
)]
AttemptNotInRun {
workflow_id: String,
run_id: String,
activity_id: u64,
attempt: u32,
},
}
impl RunScope {
pub(crate) fn from_history(history: &[Event]) -> Self {
let mut by_run: BTreeMap<String, BTreeSet<AttemptAxis>> = BTreeMap::new();
let mut current: Option<String> = None;
for event in history {
match event {
Event::WorkflowStarted { run_id, .. } => {
let key = run_id.to_string();
by_run.entry(key.clone()).or_default();
current = Some(key);
}
Event::ActivityStarted {
activity_id,
attempt,
..
} => {
if let Some(run) = current.as_ref() {
by_run
.entry(run.clone())
.or_default()
.insert((activity_id.sequence_position(), *attempt));
}
}
_ => {}
}
}
Self { by_run }
}
pub(crate) fn knows_run(&self, run_id: &RunId) -> bool {
self.by_run.contains_key(&run_id.to_string())
}
pub(crate) fn validate_handle(
&self,
workflow_id: &aion_core::WorkflowId,
run_id: &RunId,
activity_id: &ActivityId,
attempt: u32,
) -> Result<(), RunScopeError> {
let axis = (activity_id.sequence_position(), attempt);
let attempts =
self.by_run
.get(&run_id.to_string())
.ok_or_else(|| RunScopeError::UnknownRun {
workflow_id: workflow_id.to_string(),
run_id: run_id.to_string(),
})?;
if !attempts.contains(&axis) {
return Err(RunScopeError::AttemptNotInRun {
workflow_id: workflow_id.to_string(),
run_id: run_id.to_string(),
activity_id: axis.0,
attempt,
});
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use aion_core::{
ActivityId, ContentType, Event, EventEnvelope, PackageVersion, Payload, RunId, WorkflowId,
};
use chrono::{DateTime, Utc};
use uuid::Uuid;
use super::{RunScope, RunScopeError};
fn workflow_id() -> WorkflowId {
WorkflowId::new(Uuid::from_u128(1))
}
fn recorded_at(offset: i64) -> DateTime<Utc> {
DateTime::from_timestamp(1_700_000_000 + offset, 0).unwrap_or_default()
}
fn envelope(seq: u64) -> EventEnvelope {
EventEnvelope {
seq,
recorded_at: recorded_at(i64::try_from(seq).unwrap_or(0)),
workflow_id: workflow_id(),
}
}
fn empty_input() -> Payload {
Payload::new(ContentType::Json, b"{}".to_vec())
}
fn started(seq: u64, run_id: &RunId) -> Event {
Event::WorkflowStarted {
envelope: envelope(seq),
workflow_type: "chain".to_owned(),
input: empty_input(),
run_id: run_id.clone(),
parent_run_id: None,
parent_workflow_id: None,
package_version: PackageVersion::new("hash"),
}
}
fn activity_started(seq: u64, ordinal: u64, attempt: u32) -> Event {
Event::ActivityStarted {
envelope: envelope(seq),
activity_id: ActivityId::from_sequence_position(ordinal),
attempt,
}
}
fn continue_as_new_chain(first: &RunId, second: &RunId) -> Vec<Event> {
vec![
started(0, first),
activity_started(1, 0, 1),
Event::WorkflowContinuedAsNew {
envelope: envelope(2),
input: empty_input(),
workflow_type: None,
parent_run_id: first.clone(),
},
started(3, second),
activity_started(4, 0, 1),
]
}
#[test]
fn each_generation_owns_its_own_dispatched_handle() -> Result<(), RunScopeError> {
let first = RunId::new(Uuid::from_u128(11));
let second = RunId::new(Uuid::from_u128(12));
let scope = RunScope::from_history(&continue_as_new_chain(&first, &second));
assert!(scope.knows_run(&first));
assert!(scope.knows_run(&second));
scope.validate_handle(
&workflow_id(),
&first,
&ActivityId::from_sequence_position(0),
1,
)?;
scope.validate_handle(
&workflow_id(),
&second,
&ActivityId::from_sequence_position(0),
1,
)?;
Ok(())
}
#[test]
fn a_run_that_is_not_this_workflows_is_refused() {
let only = RunId::new(Uuid::from_u128(21));
let stranger = RunId::new(Uuid::from_u128(99));
let scope = RunScope::from_history(&[started(0, &only), activity_started(1, 0, 1)]);
assert!(matches!(
scope.validate_handle(
&workflow_id(),
&stranger,
&ActivityId::from_sequence_position(0),
1
),
Err(RunScopeError::UnknownRun { .. })
));
}
#[test]
fn an_attempt_this_run_never_dispatched_is_refused() {
let first = RunId::new(Uuid::from_u128(11));
let second = RunId::new(Uuid::from_u128(12));
let mut history = continue_as_new_chain(&first, &second);
history.insert(2, activity_started(9, 7, 1));
let scope = RunScope::from_history(&history);
assert!(matches!(
scope.validate_handle(
&workflow_id(),
&second,
&ActivityId::from_sequence_position(7),
1
),
Err(RunScopeError::AttemptNotInRun { .. })
));
assert!(
scope
.validate_handle(
&workflow_id(),
&first,
&ActivityId::from_sequence_position(7),
1
)
.is_ok()
);
}
#[test]
fn a_retry_attempt_is_validated_on_its_own_axis() -> Result<(), RunScopeError> {
let first = RunId::new(Uuid::from_u128(11));
let second = RunId::new(Uuid::from_u128(12));
let mut history = continue_as_new_chain(&first, &second);
history.push(activity_started(5, 0, 2));
let scope = RunScope::from_history(&history);
scope.validate_handle(
&workflow_id(),
&second,
&ActivityId::from_sequence_position(0),
2,
)?;
assert!(matches!(
scope.validate_handle(
&workflow_id(),
&first,
&ActivityId::from_sequence_position(0),
2
),
Err(RunScopeError::AttemptNotInRun { .. })
));
Ok(())
}
}