use std::sync::Arc;
use aion_core::{ActivityId, Event, Payload, WorkflowStatus};
use aion_package::ContentHash;
use aion_store::visibility::VisibilityStore;
use aion_store::{EventStore, InMemoryStore};
use serde_json::json;
use super::{ContinueAsNewContext, ContinueAsNewRequest, continue_as_new};
use crate::EngineError;
use crate::durability::Recorder;
use crate::loader::WorkflowCatalog;
use crate::registry::{
CompletionNotifier, HandleResidency, Registry, TerminalOutcome, WorkflowHandle,
WorkflowHandleParts,
};
use crate::runtime::{RuntimeConfig, RuntimeHandle};
use crate::supervision::SupervisionTree;
struct ActiveWorkflow {
store: Arc<dyn EventStore>,
visibility_store: Arc<dyn VisibilityStore>,
catalog: Arc<WorkflowCatalog>,
runtime: Arc<RuntimeHandle>,
supervision: Arc<SupervisionTree>,
registry: Arc<Registry>,
handle: WorkflowHandle,
}
fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
Payload::from_json(&json!({ "label": label }))
}
const DECLARED_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3_600);
fn workflow_catalog() -> Arc<WorkflowCatalog> {
let catalog = Arc::new(WorkflowCatalog::new());
catalog.note_loaded_workflow_with_timeout_for_test(
"checkout",
"checkout_deployed_v1",
"run",
ContentHash::from_bytes([3; 32]),
DECLARED_TIMEOUT,
);
catalog.note_loaded_workflow_with_timeout_for_test(
"checkout",
"checkout_deployed_v2",
"run",
ContentHash::from_bytes([4; 32]),
DECLARED_TIMEOUT,
);
catalog.note_loaded_workflow_for_test(
"fulfillment",
"fulfillment_deployed",
"run",
ContentHash::from_bytes([5; 32]),
);
catalog
}
async fn active_workflow() -> Result<ActiveWorkflow, Box<dyn std::error::Error>> {
let backing = Arc::new(InMemoryStore::default());
let store: Arc<dyn EventStore> = Arc::clone(&backing) as Arc<dyn EventStore>;
let visibility_store: Arc<dyn VisibilityStore> = backing;
let catalog = workflow_catalog();
let runtime = Arc::new(RuntimeHandle::new(RuntimeConfig::new(
Some(1),
crate::runtime::config::TEST_STOP_DRAIN_TIMEOUT,
))?);
runtime.register_waiting_test_module("checkout_deployed_v1", "run");
runtime.register_waiting_test_module("checkout_deployed_v2", "run");
runtime.register_waiting_test_module("fulfillment_deployed", "run");
let supervision = Arc::new(SupervisionTree::new());
let registry = Arc::new(Registry::default());
let workflow_id = aion_core::WorkflowId::new_v4();
let run_id = aion_core::RunId::new_v4();
let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(&store))
.with_visibility(run_id.clone(), Arc::clone(&visibility_store));
recorder
.record_workflow_started(
chrono::Utc::now(),
crate::durability::WorkflowStartRecord {
workflow_type: "checkout".to_owned(),
input: payload("input")?,
run_id: run_id.clone(),
parent_run_id: None,
parent_workflow_id: None,
package_version: aion_core::PackageVersion::new("a".repeat(64)),
},
)
.await?;
let deadline_id = crate::time::deadline_timer_id(&run_id)?;
recorder
.record_timer_started(
chrono::Utc::now(),
deadline_id,
chrono::Utc::now() + chrono::Duration::hours(1),
)
.await?;
crate::runtime::nif_timer_bridge::install_timer_nif_bridge(
runtime.nif_state(),
Arc::clone(®istry),
Arc::clone(&store),
tokio::runtime::Handle::current(),
runtime.signal_delivery(),
);
let pid = runtime.spawn_test_process_with_trap_exit(true)?;
let handle = WorkflowHandle::new(WorkflowHandleParts {
workflow_id: workflow_id.clone(),
run_id: run_id.clone(),
pid,
workflow_type: "checkout".to_owned(),
namespace: String::from("default"),
loaded_version: ContentHash::from_bytes([3; 32]),
cached_status: WorkflowStatus::Running,
residency: HandleResidency::Resident,
recorder,
completion: CompletionNotifier::new(),
});
registry.insert((workflow_id, run_id), handle.clone())?;
Ok(ActiveWorkflow {
store,
visibility_store,
catalog,
runtime,
supervision,
registry,
handle,
})
}
fn context(active: &ActiveWorkflow) -> ContinueAsNewContext<'_> {
ContinueAsNewContext {
store: Arc::clone(&active.store),
visibility_store: Arc::clone(&active.visibility_store),
catalog: Arc::clone(&active.catalog),
runtime: &active.runtime,
supervision: Arc::clone(&active.supervision),
registry: &active.registry,
search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
}
}
#[tokio::test]
async fn pending_activity_rejects_without_terminal_event() -> Result<(), Box<dyn std::error::Error>>
{
let active = active_workflow().await?;
{
let recorder = active.handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_activity_scheduled(
chrono::Utc::now(),
ActivityId::from_sequence_position(2),
"charge-card".to_owned(),
payload("activity")?,
String::from("default"),
None,
)
.await?;
}
let result = continue_as_new(
context(&active),
active.handle.workflow_id(),
active.handle.run_id(),
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await;
assert!(matches!(
result,
Err(EngineError::Runtime { reason }) if reason.contains("pending work")
));
let history = active
.store
.read_history(active.handle.workflow_id())
.await?;
assert!(!matches!(
history.last(),
Some(Event::WorkflowContinuedAsNew { .. })
));
assert_eq!(
active
.registry
.get(active.handle.workflow_id(), active.handle.run_id())?,
Some(active.handle.clone())
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn pending_child_rejects_without_terminal_event() -> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
{
let recorder = active.handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_child_workflow_started(
chrono::Utc::now(),
aion_core::WorkflowId::new_v4(),
"fulfillment".to_owned(),
payload("child")?,
aion_core::PackageVersion::new("a".repeat(64)),
)
.await?;
}
let result = continue_as_new(
context(&active),
active.handle.workflow_id(),
active.handle.run_id(),
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await;
assert!(matches!(
result,
Err(EngineError::Runtime { reason }) if reason.contains("pending work")
));
let history = active
.store
.read_history(active.handle.workflow_id())
.await?;
assert!(!matches!(
history.last(),
Some(Event::WorkflowContinuedAsNew { .. })
));
assert_eq!(
active
.registry
.get(active.handle.workflow_id(), active.handle.run_id())?,
Some(active.handle.clone())
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn success_records_notifies_deregisters_and_starts_new_run()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
let old_workflow_id = active.handle.workflow_id().clone();
let old_run_id = active.handle.run_id().clone();
let input = payload("next")?;
let mut receiver = active.handle.completion().subscribe();
let new_handle = continue_as_new(
context(&active),
&old_workflow_id,
&old_run_id,
ContinueAsNewRequest {
input: input.clone(),
workflow_type: None,
},
)
.await?;
receiver.changed().await?;
assert_eq!(new_handle.workflow_id(), &old_workflow_id);
assert_ne!(new_handle.run_id(), &old_run_id);
assert_eq!(new_handle.workflow_type(), "checkout");
assert_eq!(
new_handle.loaded_version(),
&ContentHash::from_bytes([4; 32]),
"the continue-as-new successor must take the latest loaded version (D1)"
);
assert_eq!(active.registry.get(&old_workflow_id, &old_run_id)?, None);
assert_eq!(
active.registry.get(&old_workflow_id, new_handle.run_id())?,
Some(new_handle.clone())
);
assert_eq!(
receiver.borrow().clone(),
Some(TerminalOutcome::ContinuedAsNew {
input: input.clone(),
workflow_type: None,
parent_run_id: old_run_id.clone(),
})
);
let history = active.store.read_history(&old_workflow_id).await?;
assert_transition_batch(&history, &input, &old_run_id, &new_handle)?;
assert!(
Arc::ptr_eq(&active.handle.recorder(), &new_handle.recorder()),
"the successor must carry the predecessor's own recorder"
);
active.runtime.shutdown()?;
Ok(())
}
fn assert_transition_batch(
history: &[Event],
input: &Payload,
old_run_id: &aion_core::RunId,
new_handle: &WorkflowHandle,
) -> Result<(), Box<dyn std::error::Error>> {
let predecessor_deadline = crate::time::deadline_timer_id(old_run_id)?;
let successor_deadline = crate::time::deadline_timer_id(new_handle.run_id())?;
match history {
[
Event::WorkflowStarted { .. },
Event::TimerStarted {
timer_id: armed_predecessor,
..
},
Event::WorkflowContinuedAsNew {
input: continued_input,
workflow_type,
parent_run_id,
..
},
Event::TimerCancelled {
timer_id: retired,
cause: aion_core::TimerCancelCause::WorkflowIntent,
..
},
Event::WorkflowStarted {
input: started_input,
workflow_type: started_type,
run_id: started_run_id,
parent_run_id: started_parent,
..
},
Event::TimerStarted {
timer_id: armed_successor,
..
},
] => {
assert_eq!(armed_predecessor, &predecessor_deadline);
assert_eq!(continued_input, input);
assert_eq!(workflow_type, &None);
assert_eq!(parent_run_id, old_run_id);
assert_eq!(
retired, &predecessor_deadline,
"the predecessor's deadline is retired IN the batch (D5)"
);
assert_eq!(started_input, input);
assert_eq!(started_type, "checkout");
assert_eq!(started_run_id, new_handle.run_id());
assert_eq!(started_parent, &Some(old_run_id.clone()));
assert_eq!(armed_successor, &successor_deadline);
}
other => {
return Err(format!("expected continue-as-new history, found {other:?}").into());
}
}
assert_eq!(
crate::time::outstanding_deadline_timer(history, old_run_id),
None,
"the predecessor deadline is retired, so failover re-arm cannot resurrect it"
);
assert_eq!(
crate::time::outstanding_deadline_timer(history, new_handle.run_id()),
Some(successor_deadline),
"the successor's own deadline stays live"
);
let boundary = &history[2..];
let stamp = *boundary
.first()
.ok_or("the boundary must have a first event")?
.recorded_at();
for (offset, event) in boundary.iter().enumerate() {
let expected_seq = u64::try_from(offset)?
.checked_add(3)
.ok_or("seq overflow")?;
assert_eq!(
event.seq(),
expected_seq,
"the boundary occupies consecutive sequences: {history:#?}"
);
assert_eq!(
*event.recorded_at(),
stamp,
"the boundary is minted from one clock read: {history:#?}"
);
}
Ok(())
}
#[tokio::test]
async fn recorded_terminal_rejects_continue_without_second_terminal_event()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
{
let recorder = active.handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_workflow_cancelled(
chrono::Utc::now(),
"caller requested cancellation".to_owned(),
)
.await?;
}
let result = continue_as_new(
context(&active),
active.handle.workflow_id(),
active.handle.run_id(),
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await;
assert!(matches!(
result,
Err(EngineError::Runtime { reason })
if reason.contains("already recorded a terminal event")
));
let history = active
.store
.read_history(active.handle.workflow_id())
.await?;
assert!(
matches!(
history.as_slice(),
[
Event::WorkflowStarted { .. },
Event::TimerStarted { .. },
Event::WorkflowCancelled { .. }
]
),
"the refused transition appended nothing: {history:#?}"
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn different_replacement_type_rejects_before_terminal_mutation()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
let result = continue_as_new(
context(&active),
active.handle.workflow_id(),
active.handle.run_id(),
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: Some("fulfillment".to_owned()),
},
)
.await;
assert!(matches!(
result,
Err(EngineError::Runtime { reason })
if reason.contains("must restart the same workflow type")
));
let history = active
.store
.read_history(active.handle.workflow_id())
.await?;
assert!(
matches!(
history.as_slice(),
[
Event::WorkflowStarted { workflow_type, .. },
Event::TimerStarted { .. }
] if workflow_type == "checkout"
),
"the refused transition appended nothing: {history:#?}"
);
assert_eq!(
active
.registry
.get(active.handle.workflow_id(), active.handle.run_id())?,
Some(active.handle.clone())
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_predecessor_arm_across_the_transition_neither_conflicts_nor_lands()
-> Result<(), Box<dyn std::error::Error>> {
use crate::durability::RunAdmission;
use crate::engine_seam::{EngineHandle, TimerWheelEntry, WorkflowProcessHandle};
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let predecessor_run = active.handle.run_id().clone();
let bridge =
crate::runtime::nif_timer_bridge::installed_timer_bridge(active.runtime.nif_state())
.map_err(|error| format!("the timer bridge must be installed: {error}"))?;
let sleep_timer = aion_core::TimerId::anonymous(9);
let fire_at = chrono::Utc::now() + chrono::Duration::hours(2);
{
let recorder = active.handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_timer_started(chrono::Utc::now(), sleep_timer.clone(), fire_at)
.await?;
}
let transition = install_transition_interleave(&active, &bridge)?;
bridge.arm_timer(TimerWheelEntry {
process: WorkflowProcessHandle::new(active.handle.pid()),
timer_id: sleep_timer.clone(),
fire_at,
})?;
let successor_run = transition
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.ok_or("the interleave hook never ran, so nothing was measured")?
.map_err(|error| format!("the transition must not fail: {error}"))?;
assert_ne!(successor_run, predecessor_run);
let head_before = {
let recorder = active.handle.recorder();
let recorder = recorder.lock().await;
assert_eq!(
recorder.admit_run_append(&predecessor_run).await?,
RunAdmission::RefusedTerminal,
"the predecessor's generation is closed, so its late appends are refused"
);
assert_eq!(
recorder.admit_run_append(&successor_run).await?,
RunAdmission::Open,
"control: the successor's own appends are still admitted"
);
recorder.current_head()
};
let history = active.store.read_history(&workflow_id).await?;
assert_eq!(
history.iter().map(aion_core::Event::seq).max(),
Some(head_before),
"the refused append moved neither the tracked head nor the durable one"
);
let continued = history
.iter()
.position(|event| matches!(event, Event::WorkflowContinuedAsNew { .. }))
.ok_or("no WorkflowContinuedAsNew in history")?;
let predecessor_deadline = crate::time::deadline_timer_id(&predecessor_run)?;
let successor_deadline = crate::time::deadline_timer_id(&successor_run)?;
match &history[continued..] {
[
Event::WorkflowContinuedAsNew { .. },
Event::TimerCancelled {
timer_id: retired, ..
},
Event::WorkflowStarted {
run_id: started, ..
},
Event::TimerStarted {
timer_id: armed, ..
},
] => {
assert_eq!(retired, &predecessor_deadline);
assert_eq!(started, &successor_run);
assert_eq!(armed, &successor_deadline);
}
other => {
return Err(format!(
"the transition must be one contiguous batch with nothing after it, found {other:?}"
)
.into());
}
}
assert!(
!history[continued..].iter().any(|event| matches!(
event,
Event::TimerStarted { timer_id, .. } if timer_id == &sleep_timer
)),
"the predecessor's sleep must not be armed into the successor's segment: {history:#?}"
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_transition_and_a_concurrent_predecessor_append_are_serialised_never_conflicting()
-> Result<(), Box<dyn std::error::Error>> {
use crate::durability::RunAdmission;
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let predecessor_run = active.handle.run_id().clone();
let sleep_timer = aion_core::TimerId::anonymous(11);
let arming = {
let recorder = active.handle.recorder();
let predecessor_run = predecessor_run.clone();
let sleep_timer = sleep_timer.clone();
tokio::spawn(async move {
let mut recorder = recorder.lock().await;
match recorder.admit_run_append(&predecessor_run).await {
Ok(RunAdmission::RefusedTerminal) => Ok(false),
Ok(RunAdmission::Open) => recorder
.record_timer_started(
chrono::Utc::now(),
sleep_timer,
chrono::Utc::now() + chrono::Duration::hours(2),
)
.await
.map(|_| true),
Err(error) => Err(error),
}
})
};
let successor = continue_as_new(
context(&active),
&workflow_id,
&predecessor_run,
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await?;
let armed = arming.await??;
let history = active.store.read_history(&workflow_id).await?;
let continued = history
.iter()
.position(|event| matches!(event, Event::WorkflowContinuedAsNew { .. }))
.ok_or("no WorkflowContinuedAsNew in history")?;
let sleep_position = history.iter().position(
|event| matches!(event, Event::TimerStarted { timer_id, .. } if timer_id == &sleep_timer),
);
match (armed, sleep_position) {
(true, Some(position)) => assert!(
position < continued,
"an admitted predecessor arm belongs before its own terminal: {history:#?}"
),
(false, None) => {}
(armed, position) => {
return Err(format!(
"an arm reported as armed={armed} left its TimerStarted at {position:?}, which is \
neither of the two orders one recorder allows: {history:#?}"
)
.into());
}
}
let successor_deadline = crate::time::deadline_timer_id(successor.run_id())?;
let started = history
.iter()
.position(|event| {
matches!(event, Event::WorkflowStarted { run_id, .. } if run_id == successor.run_id())
})
.ok_or("the successor was not started")?;
match &history[started..] {
[
Event::WorkflowStarted { .. },
Event::TimerStarted { timer_id, .. },
] => assert_eq!(timer_id, &successor_deadline),
other => {
return Err(format!("unexpected successor segment: {other:?}").into());
}
}
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn the_transition_leaves_exactly_one_handle_and_it_is_the_successors()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let predecessor_run = active.handle.run_id().clone();
let successor = continue_as_new(
context(&active),
&workflow_id,
&predecessor_run,
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await?;
assert_eq!(active.registry.get(&workflow_id, &predecessor_run)?, None);
assert_eq!(
active.registry.get(&workflow_id, successor.run_id())?,
Some(successor.clone())
);
assert_eq!(
active.registry.sole_handle(&workflow_id)?,
Some(successor.clone()),
"the workflow resolves to ONE writer, with no ambiguity to break"
);
assert_eq!(
active.registry.live_run_pid(&workflow_id)?,
Some((successor.run_id().clone(), successor.pid())),
"the live-pid index follows the successor"
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn starting_a_fresh_run_under_a_live_workflow_id_is_refused()
-> Result<(), Box<dyn std::error::Error>> {
use crate::lifecycle::start::{StartWorkflowContext, StartWorkflowOptions};
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let result = crate::lifecycle::start::start_workflow_with_options(
StartWorkflowContext {
store: Arc::clone(&active.store),
visibility_store: Arc::clone(&active.visibility_store),
catalog: Arc::clone(&active.catalog),
runtime: Arc::clone(&active.runtime),
supervision: Arc::clone(&active.supervision),
registry: Arc::clone(&active.registry),
signal_handoff: None,
search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
monitor_tokio_handle: tokio::runtime::Handle::current(),
},
"checkout",
payload("rival")?,
StartWorkflowOptions {
workflow_id: Some(workflow_id.clone()),
..StartWorkflowOptions::default()
},
)
.await;
match result {
Err(EngineError::WorkflowIdAlreadyLive {
holder_run_id,
holder_pid,
..
}) => {
assert_eq!(holder_run_id, active.handle.run_id().to_string());
assert_eq!(holder_pid, active.handle.pid());
}
other => {
return Err(format!("a rival start must be refused, got {other:?}").into());
}
}
let history = active.store.read_history(&workflow_id).await?;
assert_eq!(
history.len(),
2,
"the refusal must record nothing: {history:#?}"
);
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn the_recorder_is_never_released_between_the_terminal_and_the_successor()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let predecessor_run = active.handle.run_id().clone();
let watching = Arc::new(std::sync::atomic::AtomicBool::new(true));
let (first_snapshot_taken, observer_is_watching) = tokio::sync::oneshot::channel::<()>();
let observer = {
let recorder = active.handle.recorder();
let store = Arc::clone(&active.store);
let workflow_id = workflow_id.clone();
let watching = Arc::clone(&watching);
tokio::spawn(async move {
let mut snapshots = Vec::new();
let mut first_snapshot_taken = Some(first_snapshot_taken);
while watching.load(std::sync::atomic::Ordering::SeqCst) {
let held = recorder.lock().await;
snapshots.push(store.read_history(&workflow_id).await);
drop(held);
if let Some(report) = first_snapshot_taken.take()
&& report.send(()).is_err()
{
return snapshots;
}
tokio::task::yield_now().await;
}
snapshots
})
};
observer_is_watching.await?;
let successor = continue_as_new(
context(&active),
&workflow_id,
&predecessor_run,
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await?;
watching.store(false, std::sync::atomic::Ordering::SeqCst);
let snapshots = observer.await?;
assert!(
!snapshots.is_empty(),
"the observer must have taken at least one snapshot, or it measured nothing"
);
for snapshot in snapshots {
let snapshot = snapshot?;
let has_terminal = snapshot
.iter()
.any(|event| matches!(event, Event::WorkflowContinuedAsNew { .. }));
let has_successor = snapshot.iter().any(|event| {
matches!(event, Event::WorkflowStarted { run_id, .. } if run_id == successor.run_id())
});
assert!(
!has_terminal || has_successor,
"the recorder was released with a terminal and no successor behind it — the window a \
second recorder was seeded from: {snapshot:#?}"
);
}
active.runtime.shutdown()?;
Ok(())
}
#[tokio::test]
async fn an_earlier_generations_unsettled_activity_does_not_refuse_this_generation()
-> Result<(), Box<dyn std::error::Error>> {
let active = active_workflow().await?;
let workflow_id = active.handle.workflow_id().clone();
let second_run = open_generation_over_an_unsettled_activity(&active).await?;
let history = active.store.read_history(&workflow_id).await?;
assert!(
history.iter().any(|event| matches!(
event,
Event::ActivityScheduled { activity_id, .. }
if activity_id == &ActivityId::from_sequence_position(3)
)),
"the fixture must leave a genuinely unsettled activity behind, or this test measures \
nothing: {history:#?}"
);
let third = continue_as_new(
context(&active),
&workflow_id,
&second_run,
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await?;
{
let recorder = third.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_activity_scheduled(
chrono::Utc::now(),
ActivityId::from_sequence_position(99),
"ship-order".to_owned(),
payload("live")?,
String::from("default"),
None,
)
.await?;
}
let refused = continue_as_new(
context(&active),
&workflow_id,
third.run_id(),
ContinueAsNewRequest {
input: payload("next")?,
workflow_type: None,
},
)
.await;
assert!(
matches!(
refused,
Err(EngineError::Runtime { ref reason }) if reason.contains("pending work")
),
"an unsettled activity in the CURRENT generation must still refuse: {refused:?}"
);
active.runtime.shutdown()?;
Ok(())
}
async fn open_generation_over_an_unsettled_activity(
active: &ActiveWorkflow,
) -> Result<aion_core::RunId, Box<dyn std::error::Error>> {
use crate::lifecycle::continuation::{
ContinuationOrigin, ContinuationOutcome, ContinuationRequest, open_successor_generation,
};
use crate::lifecycle::start::StartWorkflowContext;
let first_run = active.handle.run_id().clone();
{
let recorder = active.handle.recorder();
let mut recorder = recorder.lock().await;
recorder
.record_activity_scheduled(
chrono::Utc::now(),
ActivityId::from_sequence_position(3),
"charge-card".to_owned(),
payload("stranded")?,
String::from("default"),
None,
)
.await?;
recorder
.record_workflow_continued_as_new(
chrono::Utc::now(),
payload("carry")?,
None,
first_run.clone(),
)
.await?;
}
let outcome = open_successor_generation(
&StartWorkflowContext {
store: Arc::clone(&active.store),
visibility_store: Arc::clone(&active.visibility_store),
catalog: Arc::clone(&active.catalog),
runtime: Arc::clone(&active.runtime),
supervision: Arc::clone(&active.supervision),
registry: Arc::clone(&active.registry),
signal_handoff: None,
search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
monitor_tokio_handle: tokio::runtime::Handle::current(),
},
&active.handle,
ContinuationRequest {
predecessor_run: first_run,
origin: ContinuationOrigin::TerminalAlreadyRecorded,
workflow_type: String::from("checkout"),
input: payload("carry")?,
},
)
.await?;
match outcome {
ContinuationOutcome::Opened(handle) => Ok(handle.run_id().clone()),
ContinuationOutcome::AlreadyOpen => Err("the successor must be opened by this call".into()),
}
}
type InterleavedTransition = Arc<std::sync::Mutex<Option<Result<aion_core::RunId, String>>>>;
fn install_transition_interleave(
active: &ActiveWorkflow,
bridge: &Arc<crate::runtime::nif_timer_bridge::TimerNifBridge>,
) -> Result<InterleavedTransition, Box<dyn std::error::Error>> {
let store = Arc::clone(&active.store);
let visibility_store = Arc::clone(&active.visibility_store);
let catalog = Arc::clone(&active.catalog);
let runtime = Arc::clone(&active.runtime);
let supervision = Arc::clone(&active.supervision);
let registry = Arc::clone(&active.registry);
let tokio_handle = tokio::runtime::Handle::current();
let workflow_id = active.handle.workflow_id().clone();
let predecessor_run = active.handle.run_id().clone();
let input = payload("next")?;
let outcome: InterleavedTransition = Arc::new(std::sync::Mutex::new(None));
let reported = Arc::clone(&outcome);
bridge.set_arm_interleave(Arc::new(move || {
let context = ContinueAsNewContext {
store: Arc::clone(&store),
visibility_store: Arc::clone(&visibility_store),
catalog: Arc::clone(&catalog),
runtime: &runtime,
supervision: Arc::clone(&supervision),
registry: ®istry,
search_attribute_schema: Arc::new(aion_core::SearchAttributeSchema::new()),
};
let request = ContinueAsNewRequest {
input: input.clone(),
workflow_type: None,
};
let result = std::thread::scope(|scope| {
match scope
.spawn(|| {
tokio_handle.block_on(continue_as_new(
context,
&workflow_id,
&predecessor_run,
request,
))
})
.join()
{
Ok(result) => result.map(|handle| handle.run_id().clone()),
Err(_) => Err(EngineError::Runtime {
reason: String::from("the transition thread panicked"),
}),
}
});
*reported
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(result.map_err(|error| error.to_string()));
}));
Ok(outcome)
}