use std::collections::HashSet;
use std::sync::Arc;
use aion_core::{ActivityId, Event, Payload, RunId, SearchAttributeSchema, WorkflowId};
use aion_store::EventStore;
use aion_store::visibility::VisibilityStore;
use crate::EngineError;
use crate::lifecycle::continuation::{
self, ContinuationOrigin, ContinuationOutcome, ContinuationRequest,
};
use crate::lifecycle::start::StartWorkflowContext;
use crate::loader::WorkflowCatalog;
use crate::registry::{Registry, TerminalOutcome, WorkflowHandle};
use crate::runtime::RuntimeHandle;
use crate::supervision::SupervisionTree;
pub struct ContinueAsNewContext<'a> {
pub store: Arc<dyn EventStore>,
pub visibility_store: Arc<dyn VisibilityStore>,
pub catalog: Arc<WorkflowCatalog>,
pub runtime: &'a Arc<RuntimeHandle>,
pub supervision: Arc<SupervisionTree>,
pub registry: &'a Arc<Registry>,
pub search_attribute_schema: Arc<SearchAttributeSchema>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ContinueAsNewRequest {
pub input: Payload,
pub workflow_type: Option<String>,
}
pub async fn continue_as_new(
context: ContinueAsNewContext<'_>,
id: &WorkflowId,
run: &RunId,
request: ContinueAsNewRequest,
) -> Result<WorkflowHandle, EngineError> {
let handle = registered_handle(context.registry, id, run)?;
let workflow_type = request
.workflow_type
.as_deref()
.unwrap_or(handle.workflow_type());
if workflow_type != handle.workflow_type() {
return Err(EngineError::Runtime {
reason: format!(
"continue_as_new must restart the same workflow type: current={}, requested={workflow_type}",
handle.workflow_type()
),
});
}
validate_replacement_workflow_type(&context.catalog, workflow_type)?;
let workflow_type = workflow_type.to_owned();
let outcome = continuation::open_successor_generation(
&StartWorkflowContext {
store: context.store,
visibility_store: context.visibility_store,
catalog: Arc::clone(&context.catalog),
runtime: Arc::clone(context.runtime),
supervision: context.supervision,
registry: Arc::clone(context.registry),
signal_handoff: None,
search_attribute_schema: context.search_attribute_schema,
monitor_tokio_handle: tokio::runtime::Handle::current(),
},
&handle,
ContinuationRequest {
predecessor_run: run.clone(),
origin: ContinuationOrigin::RecordsTheTerminal {
input: request.input.clone(),
workflow_type: request.workflow_type.clone(),
},
workflow_type,
input: request.input.clone(),
},
)
.await?;
let new_handle = match outcome {
ContinuationOutcome::Opened(handle) => *handle,
ContinuationOutcome::AlreadyOpen => {
return Err(EngineError::Runtime {
reason: format!(
"continue_as_new rejected: workflow {id} run {run} already has a successor \
generation, started by the run\'s own continue_as_new"
),
});
}
};
handle.completion().notify(TerminalOutcome::ContinuedAsNew {
input: request.input,
workflow_type: request.workflow_type,
parent_run_id: run.clone(),
});
Ok(new_handle)
}
pub(crate) fn guard_no_pending_work(events: &[Event]) -> Result<(), EngineError> {
let mut pending_activities = HashSet::<ActivityId>::new();
let mut pending_children = HashSet::<WorkflowId>::new();
for event in events {
match event {
Event::ActivityScheduled { activity_id, .. }
| Event::ActivityStarted { activity_id, .. }
| Event::ActivityLeased { activity_id, .. }
| Event::ActivityAdoptionOffered { activity_id, .. } => {
pending_activities.insert(activity_id.clone());
}
Event::ActivityCompleted { activity_id, .. }
| Event::ActivityFailed { activity_id, .. }
| Event::ActivityCancelled { activity_id, .. } => {
pending_activities.remove(activity_id);
}
Event::ChildWorkflowStarted {
child_workflow_id, ..
} => {
pending_children.insert(child_workflow_id.clone());
}
Event::ChildWorkflowCompleted {
child_workflow_id, ..
}
| Event::ChildWorkflowFailed {
child_workflow_id, ..
}
| Event::ChildWorkflowCancelled {
child_workflow_id, ..
} => {
pending_children.remove(child_workflow_id);
}
Event::WorkflowStarted { .. }
| Event::WorkflowCompleted { .. }
| Event::WorkflowFailed { .. }
| Event::WorkflowCancelled { .. }
| Event::WorkflowTimedOut { .. }
| Event::WorkflowContinuedAsNew { .. }
| Event::WorkflowReopened { .. }
| Event::WorkflowPaused { .. }
| Event::WorkflowResumed { .. }
| Event::SearchAttributesUpdated { .. }
| Event::ActivityAdvisoryExhausted { .. }
| Event::ActivityFallbackRouted { .. }
| Event::TimerStarted { .. }
| Event::TimerFired { .. }
| Event::TimerCancelled { .. }
| Event::WithTimeoutCompleted { .. }
| Event::SignalReceived { .. }
| Event::SignalSent { .. }
| Event::ScheduleCreated { .. }
| Event::ScheduleUpdated { .. }
| Event::SchedulePaused { .. }
| Event::ScheduleResumed { .. }
| Event::ScheduleDeleted { .. }
| Event::ScheduleTriggered { .. }
| Event::CadenceFired { .. }
| Event::IterationClosed { .. }
| Event::LoopRetired { .. }
| Event::WorkflowHatched { .. }
| Event::InvariantUnconfirmed { .. } => {}
}
}
if pending_activities.is_empty() && pending_children.is_empty() {
return Ok(());
}
Err(EngineError::Runtime {
reason: format!(
"cannot continue as new while pending work exists: {} activities ({:?}), {} child workflows ({:?})",
pending_activities.len(),
pending_activities,
pending_children.len(),
pending_children
),
})
}
fn registered_handle(
registry: &Registry,
id: &WorkflowId,
run: &RunId,
) -> Result<WorkflowHandle, EngineError> {
registry
.get(id, run)?
.ok_or_else(|| EngineError::WorkflowNotFound {
workflow_type: format!("{id}/{run}"),
})
}
fn validate_replacement_workflow_type(
catalog: &WorkflowCatalog,
workflow_type: &str,
) -> Result<(), EngineError> {
catalog
.routed(workflow_type)?
.ok_or_else(|| EngineError::WorkflowNotFound {
workflow_type: workflow_type.to_owned(),
})
.map(|_| ())
}
#[cfg(test)]
#[path = "continue_as_new_tests.rs"]
mod tests;