use std::collections::HashSet;
use std::sync::{Arc, Mutex};
use futures_util::future::join_all;
use serde_json::to_value;
use uuid::Uuid;
use ironflow_store::models::{Step, StepKind, StepStatus};
use crate::config::{WorkflowOptions, WorkflowStepConfig};
use crate::context::WorkflowContext;
use crate::context::cost::SharedCost;
use crate::context::lifecycle::check_replay_identity;
use crate::error::EngineError;
use crate::executor::SubWorkflowOutcome;
use crate::guard::{SharedGuardState, WorkflowRejection};
use crate::handler::{TypedWorkflow, WorkflowHandler};
use crate::plan::{SharedPlanRecorder, lock_plan};
use super::child::ChildStart;
use super::child_run::ChildRun;
use super::{ChildResume, leaves_step_open, recorded_child_run_id};
enum WaveSlot {
Replayed {
step_id: Uuid,
outcome: SubWorkflowOutcome,
},
Open(Box<Step>),
New(Option<Uuid>),
}
enum WaveChild {
Replayed {
step_id: Uuid,
outcome: SubWorkflowOutcome,
},
Started {
step_id: Uuid,
config: WorkflowStepConfig,
start: Result<ChildStart, EngineError>,
},
}
enum WaveResult {
Replayed {
step_id: Uuid,
outcome: SubWorkflowOutcome,
},
Ran {
step_id: Uuid,
config: Box<WorkflowStepConfig>,
run: ChildRun,
},
}
fn reject_duplicate_step_names(wave: &[(String, WorkflowStepConfig)]) -> Result<(), EngineError> {
let mut seen = HashSet::with_capacity(wave.len());
for (name, _) in wave {
if !seen.insert(name.as_str()) {
return Err(EngineError::StepConfig(format!(
"sub-workflow wave has two steps named {name:?}; give each child its own step name"
)));
}
}
Ok(())
}
impl WorkflowContext {
pub async fn workflows_with<W: TypedWorkflow>(
&mut self,
handler: &W,
children: Vec<(W::Input, WorkflowOptions)>,
) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
let mut wave = Vec::with_capacity(children.len());
for (input, options) in children {
wave.push(options.into_step_config(handler.name(), to_value(&input)?));
}
reject_duplicate_step_names(&wave)?;
if wave.is_empty() {
return Ok(Vec::new());
}
if let Some(plan) = self.plan().cloned() {
return self.plan_sub_workflow_wave(&plan, handler, wave).await;
}
self.run_sub_workflow_wave(handler, wave).await
}
async fn run_sub_workflow_wave(
&mut self,
handler: &dyn WorkflowHandler,
wave: Vec<(String, WorkflowStepConfig)>,
) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
let position = self.position;
let mut slots = Vec::with_capacity(wave.len());
for (step_name, _) in &wave {
slots.push(self.wave_slot(position, step_name)?);
}
let to_run = slots
.iter()
.filter(|slot| !matches!(slot, WaveSlot::Replayed { .. }))
.count();
if to_run == 0 {
self.position += 1;
let (step_ids, outcomes): (Vec<Uuid>, Vec<SubWorkflowOutcome>) = slots
.into_iter()
.filter_map(|slot| match slot {
WaveSlot::Replayed { step_id, outcome } => Some((step_id, outcome)),
WaveSlot::Open(_) | WaveSlot::New(_) => None,
})
.unzip();
self.last_step_ids = step_ids;
return Ok(outcomes);
}
let child_handler = self.resolve_child_handler(handler.name())?;
self.check_child_worker_tags(handler)?;
if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
let state = guard_state
.lock()
.map_err(|_| WorkflowRejection::GuardUnavailable)?;
let count = u32::try_from(to_run).unwrap_or(u32::MAX);
state.enter_wave(guard_config, handler.name(), count)?;
}
self.check_not_paused().await?;
self.position += 1;
let shared_cost = match &self.shared_cost {
Some(shared) => shared.clone(),
None => SharedCost::new(self.charged_cost_usd()),
};
let mut children = Vec::with_capacity(slots.len());
for ((step_name, config), slot) in wave.into_iter().zip(slots) {
let (step_id, resume) = match slot {
WaveSlot::Replayed { step_id, outcome } => {
children.push(WaveChild::Replayed { step_id, outcome });
continue;
}
WaveSlot::Open(step) => (
step.id,
recorded_child_run_id(&step).map(ChildResume::Suspended),
),
WaveSlot::New(interrupted) => (
self.record_workflow_step(&step_name, &config, position)
.await?,
interrupted.map(ChildResume::Interrupted),
),
};
let start = match self.branch_guard(handler.name()) {
Ok(guard_state) => {
self.prepare_child(
&config,
step_id,
resume,
child_handler.clone(),
guard_state,
Some(shared_cost.clone()),
)
.await
}
Err(err) => Err(err),
};
children.push(WaveChild::Started {
step_id,
config,
start,
});
}
let this = &*self;
let results = join_all(children.into_iter().map(|child| this.run_wave_child(child))).await;
let mut outcomes = Vec::with_capacity(results.len());
let mut step_ids = Vec::with_capacity(results.len());
let mut failure = None;
let mut suspension = None;
let mut sibling_suspensions = Vec::new();
for result in results {
let (step_id, settled) = match result {
WaveResult::Replayed { step_id, outcome } => (step_id, Ok(outcome)),
WaveResult::Ran {
step_id,
config,
run,
} => {
if matches!(&run.result, Err(err) if err.is_suspension()) {
sibling_suspensions.extend(run.sibling_suspensions);
}
let settled = self.settle_child_step(step_id, &config, run.result).await;
(step_id, settled)
}
};
step_ids.push(step_id);
match settled {
Ok(outcome) => outcomes.push(outcome),
Err(err) if leaves_step_open(&err) => {
if suspension.is_none() {
suspension = Some(err);
} else if err.is_suspension() {
sibling_suspensions.push(err);
}
}
Err(err) => {
if failure.is_none() {
failure = Some(err);
}
}
}
}
if let Some(err) = failure {
return Err(err);
}
if let Some(err) = suspension {
self.sibling_suspensions.extend(sibling_suspensions);
return Err(err);
}
self.last_step_ids = step_ids;
Ok(outcomes)
}
fn wave_slot(&mut self, position: u32, step_name: &str) -> Result<WaveSlot, EngineError> {
let key = (position, step_name.to_string());
if let Some(step) = self.replay_wave_steps.get(&key).cloned() {
check_replay_identity(&step, position, step_name, &StepKind::Workflow)?;
match step.status.state {
StepStatus::Completed => {
let outcome = self.replay_recorded_child(&step)?;
return Ok(WaveSlot::Replayed {
step_id: step.id,
outcome,
});
}
StepStatus::Running => return Ok(WaveSlot::Open(Box::new(step))),
_ => {}
}
}
let interrupted = match self.interrupted_children.get(&key) {
Some(step) => {
check_replay_identity(step, position, step_name, &StepKind::Workflow)?;
recorded_child_run_id(step)
}
None => None,
};
Ok(WaveSlot::New(interrupted))
}
fn branch_guard(&self, target: &str) -> Result<Option<SharedGuardState>, EngineError> {
let Some(guard_state) = &self.guard_state else {
return Ok(None);
};
let branch = guard_state
.lock()
.map_err(|_| WorkflowRejection::GuardUnavailable)?
.branch_into(target);
Ok(Some(Arc::new(Mutex::new(branch))))
}
async fn run_wave_child(&self, child: WaveChild) -> WaveResult {
match child {
WaveChild::Replayed { step_id, outcome } => WaveResult::Replayed { step_id, outcome },
WaveChild::Started {
step_id,
config,
start,
} => {
let run = match start {
Ok(ChildStart::Run(prepared)) => self.run_child(&config, *prepared).await,
Ok(ChildStart::Settled(outcome)) => ChildRun::from(Ok(outcome)),
Err(err) => ChildRun::from(Err(err)),
};
WaveResult::Ran {
step_id,
config: Box::new(config),
run,
}
}
}
}
async fn plan_sub_workflow_wave(
&mut self,
plan: &SharedPlanRecorder,
handler: &dyn WorkflowHandler,
wave: Vec<(String, WorkflowStepConfig)>,
) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
self.position += 1;
{
let mut recorder = lock_plan(plan);
let group = recorder.next_group();
for (step_name, _) in &wave {
recorder.record_or_stop(
step_name,
StepKind::Workflow,
&self.workflow_name,
Some(group.clone()),
)?;
}
}
let mut frontier = Vec::new();
let mut outcomes = Vec::with_capacity(wave.len());
for (step_name, config) in wave {
lock_plan(plan).set_last(vec![step_name]);
let planned = self
.expand_planned_child(plan, handler, config.payload)
.await;
frontier.extend(lock_plan(plan).last().iter().cloned());
outcomes.push(SubWorkflowOutcome::Completed(planned));
}
lock_plan(plan).set_last(frontier);
Ok(outcomes)
}
}