use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use aion_core::{
Event, PackageVersion, Payload, RunId, TimerId, WorkflowFilter, WorkflowId, WorkflowSummary,
};
use aion_store::{
EventStore, InMemoryStore, PackageRecord, PackageRouteRecord, ReadableEventStore, RunSummary,
StoreError, TimerEntry, WritableEventStore, WriteToken,
};
use chrono::{DateTime, Utc};
use super::{fire_wheel_timer, install_timer_nif_bridge, register_deadline_handler};
use crate::durability::{Recorder, WorkflowStartRecord};
use crate::registry::Registry;
use crate::runtime::{RuntimeConfig, RuntimeHandle};
use crate::time::{DeadlineHandler, DeadlineHandlerError};
type TestResult = Result<(), Box<dyn std::error::Error>>;
struct FlakyReadStore {
inner: Arc<InMemoryStore>,
fail_reads: AtomicUsize,
}
impl FlakyReadStore {
fn new(inner: Arc<InMemoryStore>) -> Self {
Self {
inner,
fail_reads: AtomicUsize::new(0),
}
}
fn fail_next_reads(&self, count: usize) {
self.fail_reads.store(count, Ordering::SeqCst);
}
fn take_read_failure(&self) -> bool {
let mut current = self.fail_reads.load(Ordering::SeqCst);
loop {
if current == 0 {
return false;
}
match self.fail_reads.compare_exchange(
current,
current - 1,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => return true,
Err(actual) => current = actual,
}
}
}
}
#[async_trait::async_trait]
impl ReadableEventStore for FlakyReadStore {
async fn read_history(&self, workflow_id: &WorkflowId) -> Result<Vec<Event>, StoreError> {
if self.take_read_failure() {
return Err(StoreError::Backend(
"simulated store outage during read_history".to_owned(),
));
}
self.inner.read_history(workflow_id).await
}
async fn read_history_from(
&self,
workflow_id: &WorkflowId,
from_seq: u64,
) -> Result<Vec<Event>, StoreError> {
self.inner.read_history_from(workflow_id, from_seq).await
}
async fn read_run_chain(
&self,
workflow_id: &WorkflowId,
) -> Result<Vec<RunSummary>, StoreError> {
self.inner.read_run_chain(workflow_id).await
}
async fn list_workflow_ids(&self) -> Result<Vec<WorkflowId>, StoreError> {
self.inner.list_workflow_ids().await
}
async fn list_active(&self) -> Result<Vec<WorkflowId>, StoreError> {
self.inner.list_active().await
}
async fn list_paused(&self) -> Result<Vec<WorkflowId>, StoreError> {
self.inner.list_paused().await
}
async fn query(&self, filter: &WorkflowFilter) -> Result<Vec<WorkflowSummary>, StoreError> {
self.inner.query(filter).await
}
async fn schedule_timer(
&self,
workflow_id: &WorkflowId,
timer_id: &TimerId,
fire_at: DateTime<Utc>,
) -> Result<(), StoreError> {
self.inner
.schedule_timer(workflow_id, timer_id, fire_at)
.await
}
async fn expired_timers(&self, as_of: DateTime<Utc>) -> Result<Vec<TimerEntry>, StoreError> {
self.inner.expired_timers(as_of).await
}
}
#[async_trait::async_trait]
impl WritableEventStore for FlakyReadStore {
async fn append(
&self,
token: WriteToken,
workflow_id: &WorkflowId,
events: &[Event],
expected_seq: u64,
) -> Result<(), StoreError> {
self.inner
.append(token, workflow_id, events, expected_seq)
.await
}
}
#[async_trait::async_trait]
impl aion_store::PackageStore for FlakyReadStore {
async fn put_package(&self, record: PackageRecord) -> Result<(), StoreError> {
self.inner.put_package(record).await
}
async fn put_package_with_routes(
&self,
record: PackageRecord,
route_workflow_types: &[String],
) -> Result<(), StoreError> {
self.inner
.put_package_with_routes(record, route_workflow_types)
.await
}
async fn list_packages(&self) -> Result<Vec<PackageRecord>, StoreError> {
self.inner.list_packages().await
}
async fn delete_package(
&self,
workflow_type: &str,
content_hash: &str,
) -> Result<(), StoreError> {
self.inner.delete_package(workflow_type, content_hash).await
}
async fn put_package_route(
&self,
workflow_type: &str,
content_hash: &str,
) -> Result<(), StoreError> {
self.inner
.put_package_route(workflow_type, content_hash)
.await
}
async fn list_package_routes(&self) -> Result<Vec<PackageRouteRecord>, StoreError> {
self.inner.list_package_routes().await
}
}
#[derive(Default)]
struct RecordingDeadlineHandler {
calls: AtomicUsize,
}
impl RecordingDeadlineHandler {
fn call_count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
#[async_trait::async_trait]
impl DeadlineHandler for RecordingDeadlineHandler {
async fn on_deadline_elapsed(
&self,
_workflow_id: WorkflowId,
_run_id: RunId,
) -> Result<(), DeadlineHandlerError> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
#[tokio::test(flavor = "multi_thread")]
async fn deadline_fire_retries_through_a_store_outage_spanning_both_reads() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let inner = Arc::new(InMemoryStore::default());
let workflow_id = WorkflowId::new_v4();
let run_id = RunId::new_v4();
let deadline_id = crate::time::deadline_timer_id(&run_id)?;
let event_store: Arc<dyn EventStore> = Arc::clone(&inner) as Arc<dyn EventStore>;
let mut recorder = Recorder::new(workflow_id.clone(), event_store);
recorder
.record_workflow_started(
Utc::now(),
WorkflowStartRecord {
workflow_type: "sleeper".to_owned(),
input: Payload::from_json(&serde_json::json!({}))?,
run_id: run_id.clone(),
parent_run_id: None,
parent_workflow_id: None,
package_version: PackageVersion::new("a".repeat(64)),
},
)
.await?;
recorder
.record_timer_started(Utc::now(), deadline_id.clone(), Utc::now())
.await?;
let flaky = Arc::new(FlakyReadStore::new(inner));
let registry = Arc::new(Registry::default());
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&flaky) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let handler = Arc::new(RecordingDeadlineHandler::default());
register_deadline_handler(runtime.nif_state(), |_| {
Arc::clone(&handler) as Arc<dyn DeadlineHandler>
})
.map_err(|error| format!("failed to register deadline handler: {error}"))?;
flaky.fail_next_reads(2);
fire_wheel_timer(
&Arc::downgrade(runtime.nif_state()),
&workflow_id,
&deadline_id,
Utc::now(),
)
.await;
assert!(
handler.call_count() >= 1,
"a later retry attempt drove the deadline fire to the handler after the double-read outage"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_wheel_teardown_during_arming_leaves_no_armed_timer() -> TestResult {
use crate::engine_seam::{EngineHandle, TimerWheelEntry};
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let store = Arc::new(InMemoryStore::default());
let registry = Arc::new(Registry::default());
let pid = 4242;
let workflow_id = seed_running_workflow(®istry, &store, pid).await?;
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let process = crate::engine_seam::WorkflowProcessHandle::new(pid);
let entry = |name: &str| -> Result<TimerWheelEntry, Box<dyn std::error::Error>> {
Ok(TimerWheelEntry {
process,
timer_id: TimerId::named(name)?,
fire_at: Utc::now() + chrono::Duration::hours(1),
})
};
bridge
.arm_timer(entry("control-arm")?)
.map_err(|error| format!("control: the fixture must be able to arm at all: {error}"))?;
assert_eq!(
bridge.armed_wheel_timers(),
1,
"control: the fixture must reach the real arming path"
);
bridge.disarm_timer(process, &TimerId::named("control-arm")?)?;
assert_eq!(bridge.armed_wheel_timers(), 0, "control: disarm cleaned up");
let torn = Arc::clone(&bridge);
bridge.set_arm_interleave(Arc::new(move || torn.shutdown_timer_wheel()));
let refused = bridge.arm_timer(entry("armed-into-a-teardown")?);
let Err(error) = refused else {
return Err(
"a wheel teardown that lands while `arm_timer` is between its gate and its \
insert must make the arm REFUSE: the task it spawned appends a durable \
`TimerFired` for a run this process may no longer own, which is a second \
writer for one workflow"
.into(),
);
};
assert!(
error.to_string().contains("torn down"),
"the refusal must name the cause, or an operator is sent looking for a missing workflow \
or a bad timer id: {error}"
);
assert_eq!(
bridge.armed_wheel_timers(),
0,
"the retracted arm must leave NOTHING in the wheel: an entry inserted into an \
already-drained map is a live durable writer with no owner left to abort it"
);
assert_eq!(
store.read_history(&workflow_id).await?.len(),
1,
"a refused arm must not have recorded anything"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_torn_down_wheel_refuses_to_append() -> TestResult {
use crate::engine_seam::EngineHandle;
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let store = Arc::new(InMemoryStore::default());
let registry = Arc::new(Registry::default());
let workflow_id = seed_running_workflow(®istry, &store, 7373).await?;
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let fired = |seq: u64| -> Result<Event, Box<dyn std::error::Error>> {
Ok(Event::TimerFired {
envelope: aion_core::EventEnvelope {
seq,
recorded_at: Utc::now(),
workflow_id: workflow_id.clone(),
},
timer_id: TimerId::named("late-fire")?,
})
};
let outcome = bridge.record_workflow_event(&workflow_id, fired(2)?)?;
assert!(
matches!(outcome, crate::engine_seam::RecordOutcome::Recorded),
"control: the bridge must be able to record at all, or the refusal below measures nothing"
);
let after_control = store.read_history(&workflow_id).await?.len();
assert_eq!(after_control, 2, "control: the fire landed in history");
bridge.shutdown_timer_wheel();
let refused = bridge.record_workflow_event(&workflow_id, fired(3)?);
let Err(error) = refused else {
return Err(
"a torn-down wheel must REFUSE its append: `abort` cannot stop a task already \
inside a poll, so without a boundary check a released engine still writes"
.into(),
);
};
assert!(
error.to_string().contains("torn down"),
"the refusal must name the cause: {error}"
);
assert!(
!error.to_string().contains("was not armed"),
"the append refusal must not report an arming failure for a timer that fired: {error}"
);
assert!(
error
.to_string()
.contains("fired, but its append was refused"),
"the append refusal must say what actually happened: {error}"
);
assert_eq!(
store.read_history(&workflow_id).await?.len(),
after_control,
"the refused append must not have grown the history"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_refused_cancel_is_not_reported_as_a_refused_fire() -> TestResult {
use aion_core::TimerCancelCause;
use crate::engine_seam::EngineHandle;
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let store = Arc::new(InMemoryStore::default());
let registry = Arc::new(Registry::default());
let workflow_id = seed_running_workflow(®istry, &store, 7575).await?;
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let cancelled = |seq: u64| -> Result<Event, Box<dyn std::error::Error>> {
Ok(Event::TimerCancelled {
envelope: aion_core::EventEnvelope {
seq,
recorded_at: Utc::now(),
workflow_id: workflow_id.clone(),
},
timer_id: TimerId::named("late-cancel")?,
cause: TimerCancelCause::WorkflowIntent,
})
};
let outcome = bridge.record_workflow_event(&workflow_id, cancelled(2)?)?;
assert!(
matches!(outcome, crate::engine_seam::RecordOutcome::Recorded),
"control: the bridge must be able to record a cancellation at all, or the refusal \
below measures nothing"
);
bridge.shutdown_timer_wheel();
let refused = bridge.record_workflow_event(&workflow_id, cancelled(3)?);
let Err(error) = refused else {
return Err("a torn-down wheel must REFUSE a cancellation append too".into());
};
let message = error.to_string();
assert!(
message.contains("torn down"),
"the refusal must name the cause: {error}"
);
assert!(
!message.contains("fired"),
"a refused cancellation must not be reported as a refused fire: {error}"
);
assert!(
message.contains("cancellation of timer"),
"the refusal must say that a CANCELLATION was what went unrecorded: {error}"
);
assert!(
message.contains("reissues it there"),
"the refusal must say how the run's intent is actually restored, rather than \
ending on the re-arm as though it were good news: {error}"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn a_refused_teardown_cancel_does_not_promise_a_reissue() -> TestResult {
use aion_core::TimerCancelCause;
use crate::engine_seam::EngineHandle;
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let store = Arc::new(InMemoryStore::default());
let registry = Arc::new(Registry::default());
let workflow_id = seed_running_workflow(®istry, &store, 7576).await?;
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let teardown_cancelled = |seq: u64| -> Result<Event, Box<dyn std::error::Error>> {
Ok(Event::TimerCancelled {
envelope: aion_core::EventEnvelope {
seq,
recorded_at: Utc::now(),
workflow_id: workflow_id.clone(),
},
timer_id: TimerId::named("teardown-cancel")?,
cause: TimerCancelCause::CancelTeardown,
})
};
let outcome = bridge.record_workflow_event(&workflow_id, teardown_cancelled(2)?)?;
assert!(
matches!(outcome, crate::engine_seam::RecordOutcome::Recorded),
"control: the bridge must be able to record a teardown cancellation at all, or the \
refusal below measures nothing"
);
bridge.shutdown_timer_wheel();
let refused = bridge.record_workflow_event(&workflow_id, teardown_cancelled(3)?);
let Err(error) = refused else {
return Err("a torn-down wheel must REFUSE a teardown cancellation append".into());
};
let message = error.to_string();
assert!(
message.contains("torn down"),
"the refusal must name the cause: {error}"
);
assert!(
!message.contains("fired"),
"a refused cancellation must not be reported as a refused fire: {error}"
);
assert!(
!message.contains("reissues it there"),
"a teardown cancel's run is terminal and never re-executes, so the refusal must not \
tell the operator to wait for the run to reissue it: {error}"
);
assert!(
message.contains("once that run's terminal lands"),
"the refusal must state the terminal as the condition it depends on rather than as a \
fact already in hand: {error}"
);
assert!(
message.contains("post-terminal"),
"the refusal must say what actually becomes of the uncancelled timer — the owning \
engine's fire is refused as post-terminal, recording nothing: {error}"
);
assert!(
message.contains("check the run's status"),
"the refusal must tell the operator what to do when the cancel that issued it also \
failed, because then the run is still live and this timer will fire: {error}"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn stand_down_is_not_a_fault() -> TestResult {
use crate::runtime::nif_timer_fire::is_wheel_teardown;
use crate::time::TimerServiceError;
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let store = Arc::new(InMemoryStore::default());
let registry = Arc::new(Registry::default());
let workflow_id = seed_running_workflow(®istry, &store, 7474).await?;
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store) as Arc<dyn EventStore>,
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let timer_id = TimerId::named("stand-down")?;
let fire_at = Utc::now();
store
.append(
WriteToken::recorder(),
&workflow_id,
&[Event::TimerStarted {
envelope: aion_core::EventEnvelope {
seq: 2,
recorded_at: fire_at,
workflow_id: workflow_id.clone(),
},
timer_id: timer_id.clone(),
fire_at,
}],
1,
)
.await?;
bridge.shutdown_timer_wheel();
let refused = bridge
.service()
.fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
.await;
let Err(error) = refused else {
return Err("a torn-down wheel must refuse the fire, or this measures nothing".into());
};
assert!(
is_wheel_teardown(&error),
"a wheel teardown must be recognised as a stand-down, or the deadline ladder \
retries it six times and then logs an error for an orderly shutdown: {error}"
);
let recorder_failure =
TimerServiceError::Engine(crate::engine_seam::EngineSeamError::Recorder {
reason: String::from("the store refused the append"),
});
assert!(
!is_wheel_teardown(&recorder_failure),
"a recorder failure is a fault worth retrying and must not be read as a stand-down"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_torn_down_wheel_does_not_time_a_run_out() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let backing = Arc::new(InMemoryStore::default());
let store: Arc<dyn EventStore> = Arc::clone(&backing) as Arc<dyn EventStore>;
let registry = Arc::new(Registry::default());
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store),
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
register_deadline_handler(runtime.nif_state(), |stand_down| {
Arc::new(crate::lifecycle::deadline::WorkflowDeadlineHandler::new(
Arc::downgrade(&runtime),
Arc::clone(&store),
Arc::clone(&backing) as Arc<dyn aion_store::visibility::VisibilityStore>,
Arc::clone(®istry),
stand_down,
)) as Arc<dyn DeadlineHandler>
})
.map_err(|error| format!("failed to register the deadline handler: {error}"))?;
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let (control_id, control_deadline) =
seed_armed_deadline(®istry, &backing, 8181, Utc::now()).await?;
bridge
.service()
.fire_timer(control_id.clone(), control_deadline, Utc::now())
.await?;
assert!(
timed_out(&store.read_history(&control_id).await?),
"control: an armed deadline on a live wheel must record WorkflowTimedOut, or the \
treatment below proves nothing"
);
let (timed_id, timed_deadline) =
seed_armed_deadline(®istry, &backing, 8282, Utc::now()).await?;
let before = store.read_history(&timed_id).await?.len();
bridge.shutdown_timer_wheel();
bridge
.service()
.fire_timer(timed_id.clone(), timed_deadline, Utc::now())
.await?;
let history = store.read_history(&timed_id).await?;
assert!(
!timed_out(&history),
"a torn-down wheel must NOT record WorkflowTimedOut: the run belongs to whichever \
engine owns it now, and a second writer for one workflow is the #119 breach: \
{history:#?}"
);
assert_eq!(
history.len(),
before,
"the stood-down deadline must append nothing at all — not the terminal, not the \
ordinary-timer retirements, not the deadline's own cancellation"
);
runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn a_torn_down_wheel_does_not_finalize_an_unregistered_timeout() -> TestResult {
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(Some(1)))?);
let backing = Arc::new(InMemoryStore::default());
let store: Arc<dyn EventStore> = Arc::clone(&backing) as Arc<dyn EventStore>;
let registry = Arc::new(Registry::default());
install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store),
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
register_deadline_handler(runtime.nif_state(), |stand_down| {
Arc::new(crate::lifecycle::deadline::WorkflowDeadlineHandler::new(
Arc::downgrade(&runtime),
Arc::clone(&store),
Arc::clone(&backing) as Arc<dyn aion_store::visibility::VisibilityStore>,
Arc::clone(®istry),
stand_down,
)) as Arc<dyn DeadlineHandler>
})
.map_err(|error| format!("failed to register the deadline handler: {error}"))?;
let bridge = super::timer_bridge(runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let (control_id, control_deadline) = seed_unfinished_timeout(&backing).await?;
bridge
.service()
.fire_timer(control_id.clone(), control_deadline.clone(), Utc::now())
.await?;
assert!(
cancelled(&store.read_history(&control_id).await?, &control_deadline),
"control: the registry-free finalizer must retire the outstanding deadline, or the \
treatment below measures a path that never ran"
);
let (stale_id, stale_deadline) = seed_unfinished_timeout(&backing).await?;
let before = store.read_history(&stale_id).await?.len();
bridge.shutdown_timer_wheel();
bridge
.service()
.fire_timer(stale_id.clone(), stale_deadline.clone(), Utc::now())
.await?;
let history = store.read_history(&stale_id).await?;
assert!(
!cancelled(&history, &stale_deadline),
"a torn-down wheel must not finalize an unregistered timeout: every append here is \
durable and the run may already be owned elsewhere: {history:#?}"
);
assert_eq!(
history.len(),
before,
"the stood-down finalizer must append nothing at all"
);
runtime.shutdown()?;
Ok(())
}
async fn seed_unfinished_timeout(
store: &Arc<InMemoryStore>,
) -> Result<(WorkflowId, TimerId), Box<dyn std::error::Error>> {
let workflow_id = WorkflowId::new_v4();
let run_id = RunId::new_v4();
let deadline_id = crate::time::deadline_timer_id(&run_id)?;
let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(store) as _);
recorder
.record_workflow_started(
Utc::now(),
WorkflowStartRecord {
workflow_type: "sleeper".to_owned(),
input: Payload::from_json(&serde_json::json!({}))?,
run_id: run_id.clone(),
parent_run_id: None,
parent_workflow_id: None,
package_version: PackageVersion::new("a".repeat(64)),
},
)
.await?;
recorder
.record_timer_started(Utc::now(), deadline_id.clone(), Utc::now())
.await?;
recorder
.record_workflow_timed_out(Utc::now(), String::from("workflow"))
.await?;
Ok((workflow_id, deadline_id))
}
fn cancelled(history: &[Event], timer_id: &TimerId) -> bool {
history
.iter()
.any(|event| matches!(event, Event::TimerCancelled { timer_id: id, .. } if id == timer_id))
}
fn timed_out(history: &[Event]) -> bool {
history
.iter()
.any(|event| matches!(event, Event::WorkflowTimedOut { .. }))
}
async fn seed_armed_deadline(
registry: &Registry,
store: &Arc<InMemoryStore>,
pid: u64,
fire_at: DateTime<Utc>,
) -> Result<(WorkflowId, TimerId), Box<dyn std::error::Error>> {
let workflow_id = seed_running_workflow(registry, store, pid).await?;
let history = store.read_history(&workflow_id).await?;
let run_id = history
.iter()
.find_map(|event| match event {
Event::WorkflowStarted { run_id, .. } => Some(run_id.clone()),
_ => None,
})
.ok_or("the seeded workflow must have a WorkflowStarted")?;
let deadline_id = crate::time::deadline_timer_id(&run_id)?;
let handle = registry
.get(&workflow_id, &run_id)?
.ok_or("the seeded workflow must be registered")?;
let recorder = handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_timer_started(fire_at, deadline_id.clone(), fire_at)
.await?;
Ok((workflow_id, deadline_id))
}
async fn seed_running_workflow(
registry: &Registry,
store: &Arc<InMemoryStore>,
pid: u64,
) -> Result<WorkflowId, Box<dyn std::error::Error>> {
use crate::registry::{
CompletionNotifier, HandleResidency, WorkflowHandle, WorkflowHandleParts,
};
use aion_core::WorkflowStatus;
let workflow_id = WorkflowId::new_v4();
let run_id = RunId::new_v4();
let started = Event::WorkflowStarted {
envelope: aion_core::EventEnvelope {
seq: 1,
recorded_at: Utc::now(),
workflow_id: workflow_id.clone(),
},
workflow_type: "sleeper".to_owned(),
input: Payload::from_json(&serde_json::json!({}))?,
run_id: run_id.clone(),
parent_run_id: None,
parent_workflow_id: None,
package_version: PackageVersion::new("a".repeat(64)),
};
store
.append(WriteToken::recorder(), &workflow_id, &[started], 0)
.await?;
let recorder = Recorder::resume_at(workflow_id.clone(), Arc::clone(store) as _, 1);
let handle = WorkflowHandle::new(WorkflowHandleParts {
workflow_id: workflow_id.clone(),
run_id: run_id.clone(),
pid,
workflow_type: "sleeper".to_owned(),
namespace: String::from("default"),
loaded_version: aion_package::ContentHash::from_bytes([9; 32]),
cached_status: WorkflowStatus::Running,
residency: HandleResidency::Resident,
recorder,
completion: CompletionNotifier::new(),
});
registry.insert((workflow_id.clone(), run_id), handle)?;
Ok(workflow_id)
}