Skip to main content

ironflow_engine/context/steps/sub_workflow/
wave.rs

1//! A wave of sub-workflows of one handler, run concurrently.
2//!
3//! Every child gets a `Workflow` step at the same position. Steps and child
4//! runs are created in list order, then the children execute concurrently in
5//! the parent's task: each child future borrows the parent context, like a
6//! single `workflow_with` does. The parent closes the steps in list order.
7
8use std::collections::HashSet;
9use std::sync::{Arc, Mutex};
10
11use futures_util::future::join_all;
12use serde_json::to_value;
13use uuid::Uuid;
14
15use ironflow_store::models::{Step, StepKind, StepStatus};
16
17use crate::config::{WorkflowOptions, WorkflowStepConfig};
18use crate::context::WorkflowContext;
19use crate::context::cost::SharedCost;
20use crate::context::lifecycle::check_replay_identity;
21use crate::error::EngineError;
22use crate::executor::SubWorkflowOutcome;
23use crate::guard::{SharedGuardState, WorkflowRejection};
24use crate::handler::{TypedWorkflow, WorkflowHandler};
25use crate::plan::{SharedPlanRecorder, lock_plan};
26
27use super::child::ChildStart;
28use super::child_run::ChildRun;
29use super::{ChildResume, leaves_step_open, recorded_child_run_id};
30
31/// Where a child of a wave stands before the wave runs.
32enum WaveSlot {
33    /// Completed in a previous execution: replayed as recorded.
34    Replayed {
35        step_id: Uuid,
36        outcome: SubWorkflowOutcome,
37    },
38    /// Left open by a child that suspended: the step is reused.
39    Open(Box<Step>),
40    /// Recorded now, re-entering the child run of a step interrupted by a
41    /// lost lease, if any.
42    New(Option<Uuid>),
43}
44
45/// A child of a wave once its step is recorded.
46enum WaveChild {
47    Replayed {
48        step_id: Uuid,
49        outcome: SubWorkflowOutcome,
50    },
51    Started {
52        step_id: Uuid,
53        config: WorkflowStepConfig,
54        start: Result<ChildStart, EngineError>,
55    },
56}
57
58/// A child of a wave once it ran.
59enum WaveResult {
60    Replayed {
61        step_id: Uuid,
62        outcome: SubWorkflowOutcome,
63    },
64    Ran {
65        step_id: Uuid,
66        config: Box<WorkflowStepConfig>,
67        run: ChildRun,
68    },
69}
70
71/// Reject a wave in which two children share a step name.
72///
73/// The step name identifies a child of the wave in the run timeline and on
74/// resume, where two children with one name would re-enter one child run.
75fn reject_duplicate_step_names(wave: &[(String, WorkflowStepConfig)]) -> Result<(), EngineError> {
76    let mut seen = HashSet::with_capacity(wave.len());
77    for (name, _) in wave {
78        if !seen.insert(name.as_str()) {
79            return Err(EngineError::StepConfig(format!(
80                "sub-workflow wave has two steps named {name:?}; give each child its own step name"
81            )));
82        }
83    }
84    Ok(())
85}
86
87impl WorkflowContext {
88    /// Execute a wave of sub-workflows of one handler, concurrently.
89    ///
90    /// Each entry starts one child run of `handler` with its input and its
91    /// [`WorkflowOptions`], as [`workflow_with`](Self::workflow_with) does:
92    /// step name, labels, concurrency key, priority and `allow_failure` are
93    /// set per child. Every child gets a `Workflow` step at the same
94    /// position, and the outcomes come back in the order of `children`.
95    ///
96    /// The child runs are created in list order, then executed concurrently;
97    /// the call returns once every child has finished or suspended. A
98    /// concurrency key already held, by an earlier sibling or by any other
99    /// active run, gives a [`SubWorkflowOutcome::Conflict`] for that entry,
100    /// replayed as-is on resume.
101    ///
102    /// The cost cap of the root run holds for the whole tree: every agent step
103    /// of every child checks it against what the root, the children and their
104    /// descendants have spent, plus the budget their running steps hold. A
105    /// wave never takes the root run past its cap; a child refused by the cap
106    /// ends `Cancelled` with `RUN_BUDGET_EXCEEDED`. The workflow guard counts
107    /// each child as one invocation.
108    ///
109    /// A child that suspends leaves its step open while its siblings run to
110    /// the end; the parent then suspends with it. On resume, completed steps
111    /// are replayed and suspended children are re-entered: no child run is
112    /// created twice. Each suspended child publishes its own event (approval
113    /// requested, signal awaited). An empty list records nothing.
114    ///
115    /// # Errors
116    ///
117    /// Before anything runs: [`EngineError::StepConfig`] when two entries share
118    /// a step name, [`EngineError::Serialization`] if an input cannot be
119    /// serialized, [`EngineError::InvalidWorkflow`] when `handler` is not
120    /// registered, and [`EngineError::WorkflowGuardRejected`] when the guard
121    /// refuses the wave (a wave that would cross `max_fan_out` is refused
122    /// whole). Returns [`EngineError::ReplayDivergence`] when the step recorded
123    /// under an entry's name has another kind.
124    ///
125    /// Once every child has settled and its step is recorded: the first child
126    /// failure in list order that `allow_failure` does not tolerate, otherwise
127    /// [`EngineError::ChildSuspended`] for the first suspended child in list
128    /// order.
129    ///
130    /// # Examples
131    ///
132    /// ```no_run
133    /// use ironflow_engine::config::WorkflowOptions;
134    /// use ironflow_engine::context::WorkflowContext;
135    /// use ironflow_engine::error::EngineError;
136    /// use ironflow_engine::executor::SubWorkflowOutcome;
137    /// use ironflow_engine::handler::{HandlerFuture, TypedWorkflow, WorkflowHandler};
138    /// use serde::{Deserialize, Serialize};
139    ///
140    /// #[derive(Serialize, Deserialize)]
141    /// struct DelegateInput {
142    ///     agent: String,
143    /// }
144    ///
145    /// struct Delegate;
146    ///
147    /// impl WorkflowHandler for Delegate {
148    ///     fn name(&self) -> &str { "delegate" }
149    ///     fn execute<'a>(&'a self, _ctx: &'a mut WorkflowContext) -> HandlerFuture<'a> {
150    ///         Box::pin(async move { Ok(()) })
151    ///     }
152    /// }
153    ///
154    /// impl TypedWorkflow for Delegate {
155    ///     type Input = DelegateInput;
156    /// }
157    ///
158    /// # async fn example(ctx: &mut WorkflowContext) -> Result<(), EngineError> {
159    /// let tour = 1;
160    /// let wave = ["alice", "bob"]
161    ///     .into_iter()
162    ///     .map(|agent| {
163    ///         let options = WorkflowOptions::new()
164    ///             .step_name(format!("delegate-{tour}-{agent}"))
165    ///             .label("agent", agent)
166    ///             .allow_failure();
167    ///         (DelegateInput { agent: agent.to_string() }, options)
168    ///     })
169    ///     .collect();
170    /// for outcome in ctx.workflows_with(&Delegate, wave).await? {
171    ///     if let SubWorkflowOutcome::Completed(child) = outcome {
172    ///         println!("run {} ended {}", child.run_id(), child.status());
173    ///     }
174    /// }
175    /// # Ok(())
176    /// # }
177    /// ```
178    pub async fn workflows_with<W: TypedWorkflow>(
179        &mut self,
180        handler: &W,
181        children: Vec<(W::Input, WorkflowOptions)>,
182    ) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
183        let mut wave = Vec::with_capacity(children.len());
184        for (input, options) in children {
185            wave.push(options.into_step_config(handler.name(), to_value(&input)?));
186        }
187        reject_duplicate_step_names(&wave)?;
188        if wave.is_empty() {
189            return Ok(Vec::new());
190        }
191
192        // Plan mode: the wave is one parallel group, every child expanded.
193        if let Some(plan) = self.plan().cloned() {
194            return self.plan_sub_workflow_wave(&plan, handler, wave).await;
195        }
196        self.run_sub_workflow_wave(handler, wave).await
197    }
198
199    /// Record, then run, a wave of `Workflow` steps at one position.
200    async fn run_sub_workflow_wave(
201        &mut self,
202        handler: &dyn WorkflowHandler,
203        wave: Vec<(String, WorkflowStepConfig)>,
204    ) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
205        let position = self.position;
206
207        // Replay first: a child whose step completed in a previous execution
208        // returns its recorded outcome and is not counted by the guard again.
209        let mut slots = Vec::with_capacity(wave.len());
210        for (step_name, _) in &wave {
211            slots.push(self.wave_slot(position, step_name)?);
212        }
213        let to_run = slots
214            .iter()
215            .filter(|slot| !matches!(slot, WaveSlot::Replayed { .. }))
216            .count();
217        if to_run == 0 {
218            self.position += 1;
219            let (step_ids, outcomes): (Vec<Uuid>, Vec<SubWorkflowOutcome>) = slots
220                .into_iter()
221                .filter_map(|slot| match slot {
222                    WaveSlot::Replayed { step_id, outcome } => Some((step_id, outcome)),
223                    WaveSlot::Open(_) | WaveSlot::New(_) => None,
224                })
225                .unzip();
226            self.last_step_ids = step_ids;
227            return Ok(outcomes);
228        }
229
230        let child_handler = self.resolve_child_handler(handler.name())?;
231        // A child runs on the worker that runs its parent: refuse it here
232        // rather than run it on a host missing what it requires.
233        self.check_child_worker_tags(handler)?;
234        // Every child of the wave is claimed at once, or none is.
235        if let (Some(guard_config), Some(guard_state)) = (&self.guard_config, &self.guard_state) {
236            let state = guard_state
237                .lock()
238                .map_err(|_| WorkflowRejection::GuardUnavailable)?;
239            let count = u32::try_from(to_run).unwrap_or(u32::MAX);
240            state.enter_wave(guard_config, handler.name(), count)?;
241        }
242        // Paused while it ran: no child is started, the run stops here.
243        self.check_not_paused().await?;
244        self.position += 1;
245
246        // The children share one live total of the tree, so the cap holds for
247        // the wave as a whole rather than for each child against a snapshot.
248        let shared_cost = match &self.shared_cost {
249            Some(shared) => shared.clone(),
250            None => SharedCost::new(self.charged_cost_usd()),
251        };
252
253        // Steps and child runs are created in list order, so a key held by an
254        // earlier sibling conflicts the same way on every execution.
255        let mut children = Vec::with_capacity(slots.len());
256        for ((step_name, config), slot) in wave.into_iter().zip(slots) {
257            let (step_id, resume) = match slot {
258                WaveSlot::Replayed { step_id, outcome } => {
259                    children.push(WaveChild::Replayed { step_id, outcome });
260                    continue;
261                }
262                WaveSlot::Open(step) => (
263                    step.id,
264                    recorded_child_run_id(&step).map(ChildResume::Suspended),
265                ),
266                WaveSlot::New(interrupted) => (
267                    self.record_workflow_step(&step_name, &config, position)
268                        .await?,
269                    interrupted.map(ChildResume::Interrupted),
270                ),
271            };
272            let start = match self.branch_guard(handler.name()) {
273                Ok(guard_state) => {
274                    self.prepare_child(
275                        &config,
276                        step_id,
277                        resume,
278                        child_handler.clone(),
279                        guard_state,
280                        Some(shared_cost.clone()),
281                    )
282                    .await
283                }
284                Err(err) => Err(err),
285            };
286            children.push(WaveChild::Started {
287                step_id,
288                config,
289                start,
290            });
291        }
292
293        let this = &*self;
294        let results = join_all(children.into_iter().map(|child| this.run_wave_child(child))).await;
295
296        // Steps are closed in list order: the first failure, otherwise the
297        // first suspension, is what the parent reports.
298        let mut outcomes = Vec::with_capacity(results.len());
299        let mut step_ids = Vec::with_capacity(results.len());
300        let mut failure = None;
301        let mut suspension = None;
302        let mut sibling_suspensions = Vec::new();
303        for result in results {
304            let (step_id, settled) = match result {
305                WaveResult::Replayed { step_id, outcome } => (step_id, Ok(outcome)),
306                WaveResult::Ran {
307                    step_id,
308                    config,
309                    run,
310                } => {
311                    if matches!(&run.result, Err(err) if err.is_suspension()) {
312                        sibling_suspensions.extend(run.sibling_suspensions);
313                    }
314                    let settled = self.settle_child_step(step_id, &config, run.result).await;
315                    (step_id, settled)
316                }
317            };
318            step_ids.push(step_id);
319            match settled {
320                Ok(outcome) => outcomes.push(outcome),
321                Err(err) if leaves_step_open(&err) => {
322                    if suspension.is_none() {
323                        suspension = Some(err);
324                    } else if err.is_suspension() {
325                        sibling_suspensions.push(err);
326                    }
327                }
328                Err(err) => {
329                    if failure.is_none() {
330                        failure = Some(err);
331                    }
332                }
333            }
334        }
335
336        if let Some(err) = failure {
337            return Err(err);
338        }
339        if let Some(err) = suspension {
340            self.sibling_suspensions.extend(sibling_suspensions);
341            return Err(err);
342        }
343        self.last_step_ids = step_ids;
344        Ok(outcomes)
345    }
346
347    /// Where the child named `step_name` stands at the wave `position`.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`EngineError::ReplayDivergence`] when the step recorded under
352    /// that name is not a `Workflow` step.
353    fn wave_slot(&mut self, position: u32, step_name: &str) -> Result<WaveSlot, EngineError> {
354        let key = (position, step_name.to_string());
355        if let Some(step) = self.replay_wave_steps.get(&key).cloned() {
356            check_replay_identity(&step, position, step_name, &StepKind::Workflow)?;
357            match step.status.state {
358                StepStatus::Completed => {
359                    let outcome = self.replay_recorded_child(&step)?;
360                    return Ok(WaveSlot::Replayed {
361                        step_id: step.id,
362                        outcome,
363                    });
364                }
365                StepStatus::Running => return Ok(WaveSlot::Open(Box::new(step))),
366                _ => {}
367            }
368        }
369        let interrupted = match self.interrupted_children.get(&key) {
370            Some(step) => {
371                check_replay_identity(step, position, step_name, &StepKind::Workflow)?;
372                recorded_child_run_id(step)
373            }
374            None => None,
375        };
376        Ok(WaveSlot::New(interrupted))
377    }
378
379    /// A guard state of its own for a child running concurrently with its
380    /// siblings, its invocation claimed with the wave: depth and call chain
381    /// per branch, invocations and tokens shared by the whole run tree.
382    fn branch_guard(&self, target: &str) -> Result<Option<SharedGuardState>, EngineError> {
383        let Some(guard_state) = &self.guard_state else {
384            return Ok(None);
385        };
386        let branch = guard_state
387            .lock()
388            .map_err(|_| WorkflowRejection::GuardUnavailable)?
389            .branch_into(target);
390        Ok(Some(Arc::new(Mutex::new(branch))))
391    }
392
393    /// Execute one child of a wave, unless it was replayed or settled at once.
394    async fn run_wave_child(&self, child: WaveChild) -> WaveResult {
395        match child {
396            WaveChild::Replayed { step_id, outcome } => WaveResult::Replayed { step_id, outcome },
397            WaveChild::Started {
398                step_id,
399                config,
400                start,
401            } => {
402                let run = match start {
403                    Ok(ChildStart::Run(prepared)) => self.run_child(&config, *prepared).await,
404                    Ok(ChildStart::Settled(outcome)) => ChildRun::from(Ok(outcome)),
405                    Err(err) => ChildRun::from(Err(err)),
406                };
407                WaveResult::Ran {
408                    step_id,
409                    config: Box::new(config),
410                    run,
411                }
412            }
413        }
414    }
415
416    /// Record a wave while planning: one parallel group, every child expanded
417    /// after its own step when the depth limit allows it.
418    async fn plan_sub_workflow_wave(
419        &mut self,
420        plan: &SharedPlanRecorder,
421        handler: &dyn WorkflowHandler,
422        wave: Vec<(String, WorkflowStepConfig)>,
423    ) -> Result<Vec<SubWorkflowOutcome>, EngineError> {
424        self.position += 1;
425        {
426            let mut recorder = lock_plan(plan);
427            let group = recorder.next_group();
428            for (step_name, _) in &wave {
429                recorder.record_or_stop(
430                    step_name,
431                    StepKind::Workflow,
432                    &self.workflow_name,
433                    Some(group.clone()),
434                )?;
435            }
436        }
437
438        // The steps after the wave depend on where every child ended.
439        let mut frontier = Vec::new();
440        let mut outcomes = Vec::with_capacity(wave.len());
441        for (step_name, config) in wave {
442            lock_plan(plan).set_last(vec![step_name]);
443            let planned = self
444                .expand_planned_child(plan, handler, config.payload)
445                .await;
446            frontier.extend(lock_plan(plan).last().iter().cloned());
447            outcomes.push(SubWorkflowOutcome::Completed(planned));
448        }
449        lock_plan(plan).set_last(frontier);
450        Ok(outcomes)
451    }
452}