1use chrono::{DateTime, Duration as ChronoDuration, Utc};
2use std::sync::Arc;
3use uuid::Uuid;
4
5use crate::error::{FlowError, Result};
6use crate::model::{
7 project_run, ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookStatus, RetryPolicy,
8 RuntimeCommand, StepFailureAction, StepStatus, WaitStatus, WorkflowRunSnapshot,
9 WorkflowRunSummary, WorkflowRunSuspension, WorkflowSpec,
10};
11use crate::observe::{FlowEventObserver, NoopFlowEventObserver};
12use crate::runtime::{FlowRuntime, StepInvocation, WorkflowInvocation};
13use crate::store::{FlowEventStore, InMemoryEventStore};
14
15mod validation;
16use validation::{
17 ensure_hook_command_matches, ensure_same_start, ensure_step_batch_valid,
18 ensure_step_command_matches, ensure_wait_command_matches, is_event_conflict, validate_run_id,
19};
20
21pub struct FlowEngineBuilder {
23 store: Arc<dyn FlowEventStore>,
24 runtime: Arc<dyn FlowRuntime>,
25 observer: Arc<dyn FlowEventObserver>,
26 max_replay_iterations: usize,
27}
28
29impl FlowEngineBuilder {
30 pub fn new(runtime: Arc<dyn FlowRuntime>) -> Self {
31 Self {
32 store: Arc::new(InMemoryEventStore::new()),
33 runtime,
34 observer: Arc::new(NoopFlowEventObserver),
35 max_replay_iterations: 1024,
36 }
37 }
38
39 pub fn with_store(mut self, store: Arc<dyn FlowEventStore>) -> Self {
40 self.store = store;
41 self
42 }
43
44 pub fn with_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
45 self.observer = observer;
46 self
47 }
48
49 pub fn with_max_replay_iterations(mut self, max_replay_iterations: usize) -> Self {
50 self.max_replay_iterations = max_replay_iterations.max(1);
51 self
52 }
53
54 pub fn build(self) -> FlowEngine {
55 FlowEngine {
56 store: self.store,
57 runtime: self.runtime,
58 observer: self.observer,
59 max_replay_iterations: self.max_replay_iterations,
60 }
61 }
62}
63
64#[derive(Clone)]
66pub struct FlowEngine {
67 store: Arc<dyn FlowEventStore>,
68 runtime: Arc<dyn FlowRuntime>,
69 observer: Arc<dyn FlowEventObserver>,
70 max_replay_iterations: usize,
71}
72
73impl FlowEngine {
74 pub fn builder(runtime: Arc<dyn FlowRuntime>) -> FlowEngineBuilder {
75 FlowEngineBuilder::new(runtime)
76 }
77
78 pub fn new(store: Arc<dyn FlowEventStore>, runtime: Arc<dyn FlowRuntime>) -> Self {
79 Self {
80 store,
81 runtime,
82 observer: Arc::new(NoopFlowEventObserver),
83 max_replay_iterations: 1024,
84 }
85 }
86
87 pub fn in_memory(runtime: Arc<dyn FlowRuntime>) -> Self {
88 Self::new(Arc::new(InMemoryEventStore::new()), runtime)
89 }
90
91 pub fn store(&self) -> Arc<dyn FlowEventStore> {
92 Arc::clone(&self.store)
93 }
94
95 pub fn observer(&self) -> Arc<dyn FlowEventObserver> {
96 Arc::clone(&self.observer)
97 }
98
99 pub async fn start(&self, spec: WorkflowSpec, input: serde_json::Value) -> Result<String> {
101 let run_id = Uuid::new_v4().to_string();
102 self.start_with_id(run_id, spec, input).await
103 }
104
105 pub async fn start_with_id(
110 &self,
111 run_id: impl Into<String>,
112 spec: WorkflowSpec,
113 input: serde_json::Value,
114 ) -> Result<String> {
115 spec.validate()?;
116 let run_id = run_id.into();
117 validate_run_id(&run_id)?;
118
119 for _ in 0..self.max_replay_iterations {
120 match self.store.list(&run_id).await {
121 Ok(history) => {
122 let snapshot = project_run(&run_id, &history)?;
123 ensure_same_start(&run_id, &snapshot, &spec, &input)?;
124 if !history
125 .iter()
126 .any(|event| matches!(event.event, FlowEvent::RunStarted))
127 {
128 match self
129 .record_event_at(&run_id, snapshot.last_sequence, FlowEvent::RunStarted)
130 .await
131 {
132 Ok(_) => {}
133 Err(err) if is_event_conflict(&err) => continue,
134 Err(err) => return Err(err),
135 }
136 }
137 match self.drive(&run_id).await {
138 Ok(_) => return Ok(run_id),
139 Err(err) if is_event_conflict(&err) => continue,
140 Err(err) => return Err(err),
141 }
142 }
143 Err(FlowError::RunNotFound(_)) => {
144 let created = match self
145 .record_event_at(
146 &run_id,
147 0,
148 FlowEvent::RunCreated {
149 spec: spec.clone(),
150 input: input.clone(),
151 },
152 )
153 .await
154 {
155 Ok(created) => created,
156 Err(err) if is_event_conflict(&err) => continue,
157 Err(err) => return Err(err),
158 };
159 match self
160 .record_event_at(&run_id, created.sequence, FlowEvent::RunStarted)
161 .await
162 {
163 Ok(_) => {}
164 Err(err) if is_event_conflict(&err) => continue,
165 Err(err) => return Err(err),
166 }
167 match self.drive(&run_id).await {
168 Ok(_) => return Ok(run_id),
169 Err(err) if is_event_conflict(&err) => continue,
170 Err(err) => return Err(err),
171 }
172 }
173 Err(err) => return Err(err),
174 }
175 }
176
177 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
178 }
179
180 pub async fn resume_wait(&self, run_id: &str, wait_id: &str) -> Result<()> {
182 for _ in 0..self.max_replay_iterations {
183 let snapshot = self.snapshot(run_id).await?;
184 if snapshot.status.is_terminal() {
185 return Err(FlowError::RunTerminal(run_id.to_string()));
186 }
187 match snapshot.waits.get(wait_id) {
188 Some(wait) if wait.status == WaitStatus::Waiting => {
189 match self
190 .record_event_at(
191 run_id,
192 snapshot.last_sequence,
193 FlowEvent::WaitCompleted {
194 wait_id: wait_id.to_string(),
195 },
196 )
197 .await
198 {
199 Ok(_) => {}
200 Err(err) if is_event_conflict(&err) => continue,
201 Err(err) => return Err(err),
202 }
203 match self.drive(run_id).await {
204 Ok(_) => return Ok(()),
205 Err(err) if is_event_conflict(&err) => continue,
206 Err(err) => return Err(err),
207 }
208 }
209 Some(_) => match self.drive(run_id).await {
210 Ok(_) => return Ok(()),
211 Err(err) if is_event_conflict(&err) => continue,
212 Err(err) => return Err(err),
213 },
214 None => {
215 return Err(FlowError::InvalidTransition(format!(
216 "wait {wait_id} does not exist for run {run_id}"
217 )))
218 }
219 }
220 }
221
222 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
223 }
224
225 pub async fn resume_hook(
227 &self,
228 run_id: &str,
229 hook_id: &str,
230 payload: serde_json::Value,
231 ) -> Result<()> {
232 for _ in 0..self.max_replay_iterations {
233 let snapshot = self.snapshot(run_id).await?;
234 if snapshot.status.is_terminal() {
235 return Err(FlowError::RunTerminal(run_id.to_string()));
236 }
237 match snapshot.hooks.get(hook_id) {
238 Some(hook) if hook.status == HookStatus::Active => {
239 match self
240 .record_event_at(
241 run_id,
242 snapshot.last_sequence,
243 FlowEvent::HookReceived {
244 hook_id: hook_id.to_string(),
245 payload: payload.clone(),
246 },
247 )
248 .await
249 {
250 Ok(_) => {}
251 Err(err) if is_event_conflict(&err) => continue,
252 Err(err) => return Err(err),
253 }
254 match self.drive(run_id).await {
255 Ok(_) => return Ok(()),
256 Err(err) if is_event_conflict(&err) => continue,
257 Err(err) => return Err(err),
258 }
259 }
260 Some(_) => match self.drive(run_id).await {
261 Ok(_) => return Ok(()),
262 Err(err) if is_event_conflict(&err) => continue,
263 Err(err) => return Err(err),
264 },
265 None => {
266 return Err(FlowError::InvalidTransition(format!(
267 "hook {hook_id} does not exist for run {run_id}"
268 )))
269 }
270 }
271 }
272
273 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
274 }
275
276 pub async fn dispose_hook(&self, run_id: &str, hook_id: &str) -> Result<()> {
284 for _ in 0..self.max_replay_iterations {
285 let snapshot = self.snapshot(run_id).await?;
286 if snapshot.status.is_terminal() {
287 return Err(FlowError::RunTerminal(run_id.to_string()));
288 }
289 match snapshot.hooks.get(hook_id) {
290 Some(hook) if hook.status == HookStatus::Active => {
291 match self
292 .record_event_at(
293 run_id,
294 snapshot.last_sequence,
295 FlowEvent::HookDisposed {
296 hook_id: hook_id.to_string(),
297 },
298 )
299 .await
300 {
301 Ok(_) => {}
302 Err(err) if is_event_conflict(&err) => continue,
303 Err(err) => return Err(err),
304 }
305 match self.drive(run_id).await {
306 Ok(_) => return Ok(()),
307 Err(err) if is_event_conflict(&err) => continue,
308 Err(err) => return Err(err),
309 }
310 }
311 Some(_) => match self.drive(run_id).await {
312 Ok(_) => return Ok(()),
313 Err(err) if is_event_conflict(&err) => continue,
314 Err(err) => return Err(err),
315 },
316 None => {
317 return Err(FlowError::InvalidTransition(format!(
318 "hook {hook_id} does not exist for run {run_id}"
319 )))
320 }
321 }
322 }
323
324 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
325 }
326
327 pub async fn resume_hook_by_token(
332 &self,
333 token: &str,
334 payload: serde_json::Value,
335 ) -> Result<(String, String)> {
336 let mut matches = Vec::new();
337 for run_id in self.store.list_run_ids().await? {
338 let snapshot = self.snapshot(&run_id).await?;
339 if snapshot.status.is_terminal() {
340 continue;
341 }
342 for hook in snapshot.hooks.values() {
343 if hook.status == HookStatus::Active && hook.token == token {
344 matches.push((run_id.clone(), hook.hook_id.clone()));
345 }
346 }
347 }
348
349 match matches.len() {
350 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
351 1 => {
352 let (run_id, hook_id) = matches.remove(0);
353 self.resume_hook(&run_id, &hook_id, payload).await?;
354 Ok((run_id, hook_id))
355 }
356 _ => Err(FlowError::InvalidTransition(format!(
357 "hook token {token:?} is active in multiple runs"
358 ))),
359 }
360 }
361
362 pub async fn dispose_hook_by_token(&self, token: &str) -> Result<(String, String)> {
367 let mut matches = Vec::new();
368 for run_id in self.store.list_run_ids().await? {
369 let snapshot = self.snapshot(&run_id).await?;
370 if snapshot.status.is_terminal() {
371 continue;
372 }
373 for hook in snapshot.hooks.values() {
374 if hook.status == HookStatus::Active && hook.token == token {
375 matches.push((run_id.clone(), hook.hook_id.clone()));
376 }
377 }
378 }
379
380 match matches.len() {
381 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
382 1 => {
383 let (run_id, hook_id) = matches.remove(0);
384 self.dispose_hook(&run_id, &hook_id).await?;
385 Ok((run_id, hook_id))
386 }
387 _ => Err(FlowError::InvalidTransition(format!(
388 "hook token {token:?} is active in multiple runs"
389 ))),
390 }
391 }
392
393 pub async fn list_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
398 let mut due = Vec::new();
399 for run_id in self.store.list_run_ids().await? {
400 let snapshot = self.snapshot(&run_id).await?;
401 if snapshot.status.is_terminal() {
402 continue;
403 }
404 for wait in snapshot.waits.values() {
405 if wait.status == WaitStatus::Waiting && wait.resume_at <= now {
406 due.push((run_id.clone(), wait.wait_id.clone()));
407 }
408 }
409 }
410 due.sort();
411 Ok(due)
412 }
413
414 pub async fn resume_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
419 let due = self.list_due_waits(now).await?;
420 let mut resumed = Vec::with_capacity(due.len());
421 for (run_id, wait_id) in due {
422 self.resume_wait(&run_id, &wait_id).await?;
423 resumed.push((run_id, wait_id));
424 }
425 Ok(resumed)
426 }
427
428 pub async fn list_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
430 let mut due = Vec::new();
431 for run_id in self.store.list_run_ids().await? {
432 let snapshot = self.snapshot(&run_id).await?;
433 if snapshot.status.is_terminal() {
434 continue;
435 }
436 for (step_id, _) in snapshot.due_retries(now) {
437 due.push((run_id.clone(), step_id));
438 }
439 }
440 due.sort();
441 Ok(due)
442 }
443
444 pub async fn resume_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
446 let due = self.list_due_retries(now).await?;
447 let mut run_ids = Vec::new();
448 for (run_id, _) in &due {
449 if !run_ids.contains(run_id) {
450 run_ids.push(run_id.clone());
451 }
452 }
453 for run_id in run_ids {
454 self.drive_at(&run_id, now).await?;
455 }
456 Ok(due)
457 }
458
459 pub async fn cancel(&self, run_id: &str, reason: Option<String>) -> Result<()> {
460 for _ in 0..self.max_replay_iterations {
461 let snapshot = self.snapshot(run_id).await?;
462 if snapshot.status.is_terminal() {
463 return Ok(());
464 }
465 match self
466 .record_event_at(
467 run_id,
468 snapshot.last_sequence,
469 FlowEvent::RunCancelled {
470 reason: reason.clone(),
471 },
472 )
473 .await
474 {
475 Ok(_) => return Ok(()),
476 Err(err) if is_event_conflict(&err) => continue,
477 Err(err) => return Err(err),
478 }
479 }
480
481 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
482 }
483
484 pub async fn snapshot(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
485 let history = self.store.list(run_id).await?;
486 project_run(run_id, &history)
487 }
488
489 pub async fn history(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
490 self.store.list(run_id).await
491 }
492
493 pub async fn list_run_ids(&self) -> Result<Vec<String>> {
494 self.store.list_run_ids().await
495 }
496
497 pub async fn list_snapshots(&self) -> Result<Vec<WorkflowRunSnapshot>> {
498 let mut snapshots = Vec::new();
499 for run_id in self.store.list_run_ids().await? {
500 snapshots.push(self.snapshot(&run_id).await?);
501 }
502 Ok(snapshots)
503 }
504
505 pub async fn run_summary(&self) -> Result<WorkflowRunSummary> {
511 let snapshots = self.list_snapshots().await?;
512 Ok(WorkflowRunSummary::from_snapshots(&snapshots))
513 }
514
515 pub async fn list_open_suspensions(
521 &self,
522 now: DateTime<Utc>,
523 ) -> Result<Vec<WorkflowRunSuspension>> {
524 let mut suspensions = Vec::new();
525 for run_id in self.store.list_run_ids().await? {
526 let snapshot = self.snapshot(&run_id).await?;
527 if snapshot.status.is_terminal() {
528 continue;
529 }
530 for wait in snapshot.waits.values() {
531 if wait.status == WaitStatus::Waiting {
532 suspensions.push(WorkflowRunSuspension::Wait {
533 run_id: run_id.clone(),
534 wait: wait.clone(),
535 due: wait.resume_at <= now,
536 });
537 }
538 }
539 for hook in snapshot.hooks.values() {
540 if hook.status == HookStatus::Active {
541 suspensions.push(WorkflowRunSuspension::Hook {
542 run_id: run_id.clone(),
543 hook: hook.clone(),
544 });
545 }
546 }
547 for step in snapshot.steps.values() {
548 if step.status == StepStatus::Pending {
549 if let Some(retry_after) = step.retry_after {
550 suspensions.push(WorkflowRunSuspension::Retry {
551 run_id: run_id.clone(),
552 step: step.clone(),
553 due: retry_after <= now,
554 });
555 }
556 }
557 }
558 }
559 suspensions.sort_by(|left, right| {
560 (left.run_id(), left.kind_order(), left.subject_id()).cmp(&(
561 right.run_id(),
562 right.kind_order(),
563 right.subject_id(),
564 ))
565 });
566 Ok(suspensions)
567 }
568
569 pub async fn next_wakeup(&self, now: DateTime<Utc>) -> Result<Option<WorkflowRunSuspension>> {
575 let mut wakeups = self.list_open_suspensions(now).await?;
576 wakeups.retain(|suspension| suspension.scheduled_at().is_some());
577 wakeups.sort_by(|left, right| {
578 (
579 left.scheduled_at(),
580 left.run_id(),
581 left.kind_order(),
582 left.subject_id(),
583 )
584 .cmp(&(
585 right.scheduled_at(),
586 right.run_id(),
587 right.kind_order(),
588 right.subject_id(),
589 ))
590 });
591 Ok(wakeups.into_iter().next())
592 }
593
594 pub async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
600 let mut hooks = Vec::new();
601 for run_id in self.store.list_run_ids().await? {
602 let snapshot = self.snapshot(&run_id).await?;
603 if snapshot.status.is_terminal() {
604 continue;
605 }
606 for hook in snapshot.hooks.values() {
607 if hook.status == HookStatus::Active {
608 hooks.push(ActiveHookSnapshot {
609 run_id: run_id.clone(),
610 hook: hook.clone(),
611 });
612 }
613 }
614 }
615 hooks.sort_by(|left, right| {
616 (left.run_id.as_str(), left.hook.hook_id.as_str())
617 .cmp(&(right.run_id.as_str(), right.hook.hook_id.as_str()))
618 });
619 Ok(hooks)
620 }
621
622 pub async fn drive(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
625 self.drive_at(run_id, Utc::now()).await
626 }
627
628 async fn drive_at(&self, run_id: &str, now: DateTime<Utc>) -> Result<WorkflowRunSnapshot> {
629 'replay: for _ in 0..self.max_replay_iterations {
630 let history = self.store.list(run_id).await?;
631 let snapshot = project_run(run_id, &history)?;
632 if snapshot.status.is_terminal()
633 || snapshot
634 .waits
635 .values()
636 .any(|wait| wait.status == WaitStatus::Waiting)
637 || snapshot
638 .hooks
639 .values()
640 .any(|hook| hook.status == HookStatus::Active)
641 || snapshot.has_future_retry(now)
642 {
643 return Ok(snapshot);
644 }
645
646 let command = self
647 .runtime
648 .run_workflow(WorkflowInvocation {
649 run_id: run_id.to_string(),
650 spec: snapshot.spec.clone(),
651 input: snapshot.input.clone(),
652 history,
653 })
654 .await?;
655
656 match command {
657 RuntimeCommand::Complete { output } => {
658 match self
659 .record_event_at(
660 run_id,
661 snapshot.last_sequence,
662 FlowEvent::RunCompleted { output },
663 )
664 .await
665 {
666 Ok(_) => {}
667 Err(err) if is_event_conflict(&err) => continue,
668 Err(err) => return Err(err),
669 }
670 return self.snapshot(run_id).await;
671 }
672 RuntimeCommand::Fail { error } => {
673 match self
674 .record_event_at(
675 run_id,
676 snapshot.last_sequence,
677 FlowEvent::RunFailed { error },
678 )
679 .await
680 {
681 Ok(_) => {}
682 Err(err) if is_event_conflict(&err) => continue,
683 Err(err) => return Err(err),
684 }
685 return self.snapshot(run_id).await;
686 }
687 RuntimeCommand::ScheduleStep {
688 step_id,
689 step_name,
690 input,
691 retry,
692 } => {
693 match self
694 .execute_step(run_id, &snapshot, step_id, step_name, input, retry)
695 .await
696 {
697 Ok(()) => {}
698 Err(err) if is_event_conflict(&err) => continue,
699 Err(err) => return Err(err),
700 }
701 }
702 RuntimeCommand::ScheduleSteps { steps } => {
703 ensure_step_batch_valid(&steps)?;
704 for step in steps {
705 let current_snapshot = self.snapshot(run_id).await?;
706 match self
707 .execute_step(
708 run_id,
709 ¤t_snapshot,
710 step.step_id,
711 step.step_name,
712 step.input,
713 step.retry,
714 )
715 .await
716 {
717 Ok(()) => {}
718 Err(err) if is_event_conflict(&err) => continue 'replay,
719 Err(err) => return Err(err),
720 }
721 }
722 }
723 RuntimeCommand::WaitUntil { wait_id, resume_at } => {
724 match snapshot.waits.get(&wait_id) {
725 Some(wait) => {
726 ensure_wait_command_matches(run_id, wait, resume_at)?;
727 if wait.status == WaitStatus::Completed {
728 continue;
729 }
730 return self.snapshot(run_id).await;
731 }
732 None => {
733 match self
734 .record_event_at(
735 run_id,
736 snapshot.last_sequence,
737 FlowEvent::WaitCreated { wait_id, resume_at },
738 )
739 .await
740 {
741 Ok(_) => {}
742 Err(err) if is_event_conflict(&err) => continue,
743 Err(err) => return Err(err),
744 }
745 return self.snapshot(run_id).await;
746 }
747 }
748 }
749 RuntimeCommand::CreateHook {
750 hook_id,
751 token,
752 metadata,
753 } => match snapshot.hooks.get(&hook_id) {
754 Some(hook) => {
755 ensure_hook_command_matches(run_id, hook, &token, &metadata)?;
756 if matches!(hook.status, HookStatus::Received | HookStatus::Disposed) {
757 continue;
758 }
759 return self.snapshot(run_id).await;
760 }
761 None => {
762 self.ensure_hook_token_available(run_id, &hook_id, &token)
763 .await?;
764 match self
765 .record_event_at(
766 run_id,
767 snapshot.last_sequence,
768 FlowEvent::HookCreated {
769 hook_id,
770 token,
771 metadata,
772 },
773 )
774 .await
775 {
776 Ok(_) => {}
777 Err(err) if is_event_conflict(&err) => continue,
778 Err(err) => return Err(err),
779 }
780 return self.snapshot(run_id).await;
781 }
782 },
783 }
784 }
785
786 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
787 }
788
789 async fn record_event_at(
790 &self,
791 run_id: &str,
792 expected_sequence: u64,
793 event: FlowEvent,
794 ) -> Result<FlowEventEnvelope> {
795 let envelope = self
796 .store
797 .append_if_sequence(run_id, expected_sequence, event)
798 .await?;
799 self.observer.observe(envelope.clone()).await;
800 Ok(envelope)
801 }
802
803 async fn ensure_hook_token_available(
804 &self,
805 run_id: &str,
806 hook_id: &str,
807 token: &str,
808 ) -> Result<()> {
809 for existing_run_id in self.store.list_run_ids().await? {
810 let snapshot = self.snapshot(&existing_run_id).await?;
811 if snapshot.status.is_terminal() {
812 continue;
813 }
814 for hook in snapshot.hooks.values() {
815 if hook.status != HookStatus::Active || hook.token != token {
816 continue;
817 }
818 if existing_run_id == run_id && hook.hook_id == hook_id {
819 continue;
820 }
821 return Err(FlowError::HookTokenConflict {
822 token: token.to_string(),
823 existing_run_id,
824 existing_hook_id: hook.hook_id.clone(),
825 });
826 }
827 }
828 Ok(())
829 }
830
831 async fn execute_step(
832 &self,
833 run_id: &str,
834 snapshot: &WorkflowRunSnapshot,
835 step_id: String,
836 step_name: String,
837 input: serde_json::Value,
838 retry: RetryPolicy,
839 ) -> Result<()> {
840 let mut expected_sequence = snapshot.last_sequence;
841 if let Some(step) = snapshot.steps.get(&step_id) {
842 ensure_step_command_matches(run_id, step, &step_name, &input, retry)?;
843 if matches!(step.status, StepStatus::Completed | StepStatus::Failed) {
844 return Ok(());
845 }
846 } else {
847 let envelope = self
848 .record_event_at(
849 run_id,
850 expected_sequence,
851 FlowEvent::StepCreated {
852 step_id: step_id.clone(),
853 step_name: step_name.clone(),
854 input: input.clone(),
855 retry,
856 },
857 )
858 .await?;
859 expected_sequence = envelope.sequence;
860 }
861
862 let max_attempts = retry.max_attempts.max(1);
863 let mut attempt = snapshot
864 .steps
865 .get(&step_id)
866 .map(|step| step.attempt)
867 .unwrap_or(0);
868
869 loop {
870 attempt += 1;
871 let started = self
872 .record_event_at(
873 run_id,
874 expected_sequence,
875 FlowEvent::StepStarted {
876 step_id: step_id.clone(),
877 attempt,
878 },
879 )
880 .await?;
881 expected_sequence = started.sequence;
882
883 let history = self.store.list(run_id).await?;
884 let invocation = StepInvocation {
885 run_id: run_id.to_string(),
886 step_id: step_id.clone(),
887 step_name: step_name.clone(),
888 input: input.clone(),
889 history,
890 };
891
892 match self.runtime.run_step(invocation).await {
893 Ok(output) => {
894 self.record_event_at(
895 run_id,
896 expected_sequence,
897 FlowEvent::StepCompleted { step_id, output },
898 )
899 .await?;
900 return Ok(());
901 }
902 Err(err) if attempt < max_attempts => {
903 let retry_after = if retry.delay_ms > 0 {
904 Some(Utc::now() + ChronoDuration::milliseconds(retry.delay_ms as i64))
905 } else {
906 None
907 };
908 let retrying = self
909 .record_event_at(
910 run_id,
911 expected_sequence,
912 FlowEvent::StepRetrying {
913 step_id: step_id.clone(),
914 attempt,
915 error: err.to_string(),
916 retry_after,
917 },
918 )
919 .await?;
920 expected_sequence = retrying.sequence;
921 if retry_after.is_some() {
922 return Ok(());
923 }
924 }
925 Err(err) => {
926 let error = err.to_string();
927 let failed = self
928 .record_event_at(
929 run_id,
930 expected_sequence,
931 FlowEvent::StepFailed {
932 step_id: step_id.clone(),
933 attempt,
934 error: error.clone(),
935 },
936 )
937 .await?;
938 if retry.on_exhausted == StepFailureAction::ContinueWorkflow {
939 return Ok(());
940 }
941 self.record_event_at(run_id, failed.sequence, FlowEvent::RunFailed { error })
942 .await?;
943 return Ok(());
944 }
945 }
946 }
947 }
948}