1use chrono::{DateTime, Utc};
2use std::sync::Arc;
3use uuid::Uuid;
4
5use crate::error::{FlowError, Result};
6use crate::model::{
7 project_run, ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookStatus, RuntimeCommand,
8 ScheduledWakeup, ScheduledWakeupKind, StepStatus, WaitStatus, WorkflowRunSnapshot,
9 WorkflowRunStatus, WorkflowRunSummary, WorkflowRunSuspension, WorkflowSpec,
10};
11use crate::observe::{FlowEventObserver, NoopFlowEventObserver};
12use crate::runtime::{FlowRuntime, WorkflowInvocation};
13use crate::store::{scheduled_wakeups_for_snapshot, FlowEventStore, InMemoryEventStore};
14
15mod operations;
16mod steps;
17mod validation;
18use steps::{interrupted_retry_exhaustion_event, StepExecutionContext};
19use validation::{
20 ensure_child_operation_matches, ensure_hook_command_matches, ensure_progress_matches,
21 ensure_retry_policy_valid, ensure_same_start, ensure_step_batch_valid,
22 ensure_step_command_matches, ensure_wait_command_matches, is_event_conflict, validate_run_id,
23};
24
25pub struct FlowEngineBuilder {
27 store: Arc<dyn FlowEventStore>,
28 runtime: Arc<dyn FlowRuntime>,
29 observer: Arc<dyn FlowEventObserver>,
30 max_replay_iterations: usize,
31}
32
33impl FlowEngineBuilder {
34 pub fn new(runtime: Arc<dyn FlowRuntime>) -> Self {
35 Self {
36 store: Arc::new(InMemoryEventStore::new()),
37 runtime,
38 observer: Arc::new(NoopFlowEventObserver),
39 max_replay_iterations: 1024,
40 }
41 }
42
43 pub fn with_store(mut self, store: Arc<dyn FlowEventStore>) -> Self {
44 self.store = store;
45 self
46 }
47
48 pub fn with_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
49 self.observer = observer;
50 self
51 }
52
53 pub fn with_max_replay_iterations(mut self, max_replay_iterations: usize) -> Self {
54 self.max_replay_iterations = max_replay_iterations.max(1);
55 self
56 }
57
58 pub fn build(self) -> FlowEngine {
59 FlowEngine {
60 store: self.store,
61 runtime: self.runtime,
62 observer: self.observer,
63 max_replay_iterations: self.max_replay_iterations,
64 }
65 }
66}
67
68#[derive(Clone)]
70pub struct FlowEngine {
71 store: Arc<dyn FlowEventStore>,
72 runtime: Arc<dyn FlowRuntime>,
73 observer: Arc<dyn FlowEventObserver>,
74 max_replay_iterations: usize,
75}
76
77impl FlowEngine {
78 pub fn builder(runtime: Arc<dyn FlowRuntime>) -> FlowEngineBuilder {
79 FlowEngineBuilder::new(runtime)
80 }
81
82 pub fn new(store: Arc<dyn FlowEventStore>, runtime: Arc<dyn FlowRuntime>) -> Self {
83 Self {
84 store,
85 runtime,
86 observer: Arc::new(NoopFlowEventObserver),
87 max_replay_iterations: 1024,
88 }
89 }
90
91 pub fn in_memory(runtime: Arc<dyn FlowRuntime>) -> Self {
92 Self::new(Arc::new(InMemoryEventStore::new()), runtime)
93 }
94
95 pub fn store(&self) -> Arc<dyn FlowEventStore> {
96 Arc::clone(&self.store)
97 }
98
99 pub fn observer(&self) -> Arc<dyn FlowEventObserver> {
100 Arc::clone(&self.observer)
101 }
102
103 pub async fn start(&self, spec: WorkflowSpec, input: serde_json::Value) -> Result<String> {
105 let run_id = Uuid::new_v4().to_string();
106 self.start_with_id(run_id, spec, input).await
107 }
108
109 pub async fn start_with_id(
114 &self,
115 run_id: impl Into<String>,
116 spec: WorkflowSpec,
117 input: serde_json::Value,
118 ) -> Result<String> {
119 spec.validate()?;
120 let run_id = run_id.into();
121 validate_run_id(&run_id)?;
122
123 for _ in 0..self.max_replay_iterations {
124 match self.store.list(&run_id).await {
125 Ok(history) => {
126 let snapshot = project_run(&run_id, &history)?;
127 ensure_same_start(&run_id, &snapshot, &spec, &input)?;
128 if snapshot.status == WorkflowRunStatus::Pending {
129 match self
130 .record_event_at(&run_id, snapshot.last_sequence, FlowEvent::RunStarted)
131 .await
132 {
133 Ok(_) => {}
134 Err(err) if is_event_conflict(&err) => continue,
135 Err(err) => return Err(err),
136 }
137 }
138 match self.drive(&run_id).await {
139 Ok(_) => return Ok(run_id),
140 Err(err) if is_event_conflict(&err) => continue,
141 Err(err) => return Err(err),
142 }
143 }
144 Err(FlowError::RunNotFound(_)) => {
145 let created = match self
146 .record_event_at(
147 &run_id,
148 0,
149 FlowEvent::RunCreated {
150 spec: spec.clone(),
151 input: input.clone(),
152 },
153 )
154 .await
155 {
156 Ok(created) => created,
157 Err(err) if is_event_conflict(&err) => continue,
158 Err(err) => return Err(err),
159 };
160 match self
161 .record_event_at(&run_id, created.sequence, FlowEvent::RunStarted)
162 .await
163 {
164 Ok(_) => {}
165 Err(err) if is_event_conflict(&err) => continue,
166 Err(err) => return Err(err),
167 }
168 match self.drive(&run_id).await {
169 Ok(_) => return Ok(run_id),
170 Err(err) if is_event_conflict(&err) => continue,
171 Err(err) => return Err(err),
172 }
173 }
174 Err(err) => return Err(err),
175 }
176 }
177
178 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
179 }
180
181 pub async fn resume_wait(&self, run_id: &str, wait_id: &str) -> Result<()> {
183 for _ in 0..self.max_replay_iterations {
184 let snapshot = self.snapshot(run_id).await?;
185 if snapshot.status.is_terminal() {
186 return Err(FlowError::RunTerminal(run_id.to_string()));
187 }
188 match snapshot.waits.get(wait_id) {
189 Some(wait) if wait.status == WaitStatus::Waiting => {
190 match self
191 .record_event_at(
192 run_id,
193 snapshot.last_sequence,
194 FlowEvent::WaitCompleted {
195 wait_id: wait_id.to_string(),
196 },
197 )
198 .await
199 {
200 Ok(_) => {}
201 Err(err) if is_event_conflict(&err) => continue,
202 Err(err) => return Err(err),
203 }
204 match self.drive(run_id).await {
205 Ok(_) => return Ok(()),
206 Err(err) if is_event_conflict(&err) => continue,
207 Err(err) => return Err(err),
208 }
209 }
210 Some(_) => match self.drive(run_id).await {
211 Ok(_) => return Ok(()),
212 Err(err) if is_event_conflict(&err) => continue,
213 Err(err) => return Err(err),
214 },
215 None => {
216 return Err(FlowError::InvalidTransition(format!(
217 "wait {wait_id} does not exist for run {run_id}"
218 )))
219 }
220 }
221 }
222
223 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
224 }
225
226 pub async fn resume_hook(
228 &self,
229 run_id: &str,
230 hook_id: &str,
231 payload: serde_json::Value,
232 ) -> Result<()> {
233 for _ in 0..self.max_replay_iterations {
234 let snapshot = self.snapshot(run_id).await?;
235 if snapshot.status.is_terminal() {
236 return Err(FlowError::RunTerminal(run_id.to_string()));
237 }
238 match snapshot.hooks.get(hook_id) {
239 Some(hook) if hook.status == HookStatus::Active => {
240 match self
241 .record_event_at(
242 run_id,
243 snapshot.last_sequence,
244 FlowEvent::HookReceived {
245 hook_id: hook_id.to_string(),
246 payload: payload.clone(),
247 },
248 )
249 .await
250 {
251 Ok(_) => {}
252 Err(err) if is_event_conflict(&err) => continue,
253 Err(err) => return Err(err),
254 }
255 match self.drive(run_id).await {
256 Ok(_) => return Ok(()),
257 Err(err) if is_event_conflict(&err) => continue,
258 Err(err) => return Err(err),
259 }
260 }
261 Some(_) => match self.drive(run_id).await {
262 Ok(_) => return Ok(()),
263 Err(err) if is_event_conflict(&err) => continue,
264 Err(err) => return Err(err),
265 },
266 None => {
267 return Err(FlowError::InvalidTransition(format!(
268 "hook {hook_id} does not exist for run {run_id}"
269 )))
270 }
271 }
272 }
273
274 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
275 }
276
277 pub async fn dispose_hook(&self, run_id: &str, hook_id: &str) -> Result<()> {
285 for _ in 0..self.max_replay_iterations {
286 let snapshot = self.snapshot(run_id).await?;
287 if snapshot.status.is_terminal() {
288 return Err(FlowError::RunTerminal(run_id.to_string()));
289 }
290 match snapshot.hooks.get(hook_id) {
291 Some(hook) if hook.status == HookStatus::Active => {
292 match self
293 .record_event_at(
294 run_id,
295 snapshot.last_sequence,
296 FlowEvent::HookDisposed {
297 hook_id: hook_id.to_string(),
298 },
299 )
300 .await
301 {
302 Ok(_) => {}
303 Err(err) if is_event_conflict(&err) => continue,
304 Err(err) => return Err(err),
305 }
306 match self.drive(run_id).await {
307 Ok(_) => return Ok(()),
308 Err(err) if is_event_conflict(&err) => continue,
309 Err(err) => return Err(err),
310 }
311 }
312 Some(_) => match self.drive(run_id).await {
313 Ok(_) => return Ok(()),
314 Err(err) if is_event_conflict(&err) => continue,
315 Err(err) => return Err(err),
316 },
317 None => {
318 return Err(FlowError::InvalidTransition(format!(
319 "hook {hook_id} does not exist for run {run_id}"
320 )))
321 }
322 }
323 }
324
325 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
326 }
327
328 pub async fn resume_hook_by_token(
333 &self,
334 token: &str,
335 payload: serde_json::Value,
336 ) -> Result<(String, String)> {
337 let mut matches = self
338 .store
339 .find_active_hooks_by_token(token)
340 .await?
341 .into_iter()
342 .map(|active| (active.run_id, active.hook.hook_id))
343 .collect::<Vec<_>>();
344
345 match matches.len() {
346 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
347 1 => {
348 let (run_id, hook_id) = matches.remove(0);
349 self.resume_hook(&run_id, &hook_id, payload).await?;
350 Ok((run_id, hook_id))
351 }
352 _ => Err(FlowError::InvalidTransition(
353 "hook token is active in multiple runs (value redacted)".to_string(),
354 )),
355 }
356 }
357
358 pub async fn dispose_hook_by_token(&self, token: &str) -> Result<(String, String)> {
363 let mut matches = self
364 .store
365 .find_active_hooks_by_token(token)
366 .await?
367 .into_iter()
368 .map(|active| (active.run_id, active.hook.hook_id))
369 .collect::<Vec<_>>();
370
371 match matches.len() {
372 0 => Err(FlowError::HookTokenNotFound(token.to_string())),
373 1 => {
374 let (run_id, hook_id) = matches.remove(0);
375 self.dispose_hook(&run_id, &hook_id).await?;
376 Ok((run_id, hook_id))
377 }
378 _ => Err(FlowError::InvalidTransition(
379 "hook token is active in multiple runs (value redacted)".to_string(),
380 )),
381 }
382 }
383
384 pub async fn list_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
389 let mut due = self
390 .list_due_wakeups(now)
391 .await?
392 .into_iter()
393 .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Wait)
394 .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
395 .collect::<Vec<_>>();
396 due.sort();
397 Ok(due)
398 }
399
400 pub async fn resume_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
405 let due = self.list_due_waits(now).await?;
406 let mut resumed = Vec::with_capacity(due.len());
407 for (run_id, wait_id) in due {
408 self.resume_wait(&run_id, &wait_id).await?;
409 resumed.push((run_id, wait_id));
410 }
411 Ok(resumed)
412 }
413
414 pub async fn list_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
416 let mut due = self
417 .list_due_wakeups(now)
418 .await?
419 .into_iter()
420 .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Retry)
421 .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
422 .collect::<Vec<_>>();
423 due.sort();
424 Ok(due)
425 }
426
427 pub async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
429 let mut wakeups = self.store.list_due_wakeups(now).await?;
430 wakeups.sort_by(|left, right| {
431 (left.kind, left.run_id.as_str(), left.subject_id.as_str()).cmp(&(
432 right.kind,
433 right.run_id.as_str(),
434 right.subject_id.as_str(),
435 ))
436 });
437 Ok(wakeups)
438 }
439
440 pub async fn resume_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
442 let due = self.list_due_retries(now).await?;
443 let mut run_ids = Vec::new();
444 for (run_id, _) in &due {
445 if !run_ids.contains(run_id) {
446 run_ids.push(run_id.clone());
447 }
448 }
449 for run_id in run_ids {
450 self.drive_at(&run_id, now).await?;
451 }
452 Ok(due)
453 }
454
455 pub async fn resume_scheduled_run(
462 &self,
463 run_id: &str,
464 now: DateTime<Utc>,
465 ) -> Result<Vec<ScheduledWakeup>> {
466 let history = self.store.list(run_id).await?;
467 let snapshot = project_run(run_id, &history)?;
468 let due = scheduled_wakeups_for_snapshot(&snapshot)
469 .into_iter()
470 .filter(|wakeup| wakeup.scheduled_at <= now)
471 .collect::<Vec<_>>();
472
473 let due_wait_ids = due
474 .iter()
475 .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Wait)
476 .map(|wakeup| wakeup.subject_id.clone())
477 .collect::<Vec<_>>();
478 let has_due_retries = due
479 .iter()
480 .any(|wakeup| wakeup.kind == ScheduledWakeupKind::Retry);
481
482 for wait_id in due_wait_ids {
483 self.resume_wait(run_id, &wait_id).await?;
484 }
485 if has_due_retries {
486 self.drive_at(run_id, now).await?;
487 }
488
489 Ok(due)
490 }
491
492 pub async fn snapshot(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
493 let history = self.store.list(run_id).await?;
494 project_run(run_id, &history)
495 }
496
497 pub async fn history(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
498 self.store.list(run_id).await
499 }
500
501 pub async fn list_run_ids(&self) -> Result<Vec<String>> {
502 self.store.list_run_ids().await
503 }
504
505 pub async fn list_snapshots(&self) -> Result<Vec<WorkflowRunSnapshot>> {
506 let mut snapshots = Vec::new();
507 for run_id in self.store.list_run_ids().await? {
508 snapshots.push(self.snapshot(&run_id).await?);
509 }
510 Ok(snapshots)
511 }
512
513 pub async fn run_summary(&self) -> Result<WorkflowRunSummary> {
519 let snapshots = self.list_snapshots().await?;
520 Ok(WorkflowRunSummary::from_snapshots(&snapshots))
521 }
522
523 pub async fn list_open_suspensions(
529 &self,
530 now: DateTime<Utc>,
531 ) -> Result<Vec<WorkflowRunSuspension>> {
532 let mut suspensions = Vec::new();
533 for run_id in self.store.list_run_ids().await? {
534 let snapshot = self.snapshot(&run_id).await?;
535 if snapshot.status.is_terminal() {
536 continue;
537 }
538 for wait in snapshot.waits.values() {
539 if wait.status == WaitStatus::Waiting {
540 suspensions.push(WorkflowRunSuspension::Wait {
541 run_id: run_id.clone(),
542 wait: wait.clone(),
543 due: wait.resume_at <= now,
544 });
545 }
546 }
547 for hook in snapshot.hooks.values() {
548 if hook.status == HookStatus::Active {
549 suspensions.push(WorkflowRunSuspension::Hook {
550 run_id: run_id.clone(),
551 hook: hook.clone(),
552 });
553 }
554 }
555 for step in snapshot.steps.values() {
556 if step.status == StepStatus::Pending {
557 if let Some(retry_after) = step.retry_after {
558 suspensions.push(WorkflowRunSuspension::Retry {
559 run_id: run_id.clone(),
560 step: step.clone(),
561 due: retry_after <= now,
562 });
563 }
564 }
565 }
566 }
567 suspensions.sort_by(|left, right| {
568 (left.run_id(), left.kind_order(), left.subject_id()).cmp(&(
569 right.run_id(),
570 right.kind_order(),
571 right.subject_id(),
572 ))
573 });
574 Ok(suspensions)
575 }
576
577 pub async fn next_wakeup(&self, now: DateTime<Utc>) -> Result<Option<WorkflowRunSuspension>> {
583 for _ in 0..2 {
584 let Some(wakeup) = self.store.next_scheduled_wakeup().await? else {
585 return Ok(None);
586 };
587 match self.snapshot(&wakeup.run_id).await {
588 Ok(snapshot) => {
589 if let Some(suspension) = resolve_scheduled_wakeup(&snapshot, &wakeup, now) {
590 return Ok(Some(suspension));
591 }
592 }
593 Err(FlowError::RunNotFound(_)) => {}
594 Err(error) => return Err(error),
595 }
596 }
597
598 self.next_wakeup_by_replay(now).await
599 }
600
601 async fn next_wakeup_by_replay(
602 &self,
603 now: DateTime<Utc>,
604 ) -> Result<Option<WorkflowRunSuspension>> {
605 let mut wakeups = self.list_open_suspensions(now).await?;
606 wakeups.retain(|suspension| suspension.scheduled_at().is_some());
607 wakeups.sort_by(|left, right| {
608 (
609 left.scheduled_at(),
610 left.run_id(),
611 left.kind_order(),
612 left.subject_id(),
613 )
614 .cmp(&(
615 right.scheduled_at(),
616 right.run_id(),
617 right.kind_order(),
618 right.subject_id(),
619 ))
620 });
621 Ok(wakeups.into_iter().next())
622 }
623
624 pub async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
630 self.store.list_active_hooks().await
631 }
632
633 pub async fn drive(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
636 self.drive_at(run_id, Utc::now()).await
637 }
638
639 async fn drive_at(&self, run_id: &str, now: DateTime<Utc>) -> Result<WorkflowRunSnapshot> {
640 'replay: for _ in 0..self.max_replay_iterations {
641 let history = self.store.list(run_id).await?;
642 let snapshot = project_run(run_id, &history)?;
643 if let Some(event) = interrupted_retry_exhaustion_event(&snapshot, &history) {
644 match self
645 .record_event_at(run_id, snapshot.last_sequence, event)
646 .await
647 {
648 Ok(_) => continue,
649 Err(err) if is_event_conflict(&err) => continue,
650 Err(err) => return Err(err),
651 }
652 }
653 if snapshot.status.is_terminal()
654 || snapshot
655 .waits
656 .values()
657 .any(|wait| wait.status == WaitStatus::Waiting)
658 || snapshot
659 .hooks
660 .values()
661 .any(|hook| hook.status == HookStatus::Active)
662 || (snapshot.has_future_retry(now) && snapshot.due_retries(now).is_empty())
663 {
664 return Ok(snapshot);
665 }
666
667 let command = self
668 .runtime
669 .run_workflow(WorkflowInvocation {
670 run_id: run_id.to_string(),
671 spec: snapshot.spec.clone(),
672 input: snapshot.input.clone(),
673 history,
674 })
675 .await?;
676
677 match command {
678 RuntimeCommand::Complete { output } => {
679 if snapshot.status == WorkflowRunStatus::Cancelling {
680 return Err(FlowError::InvalidTransition(format!(
681 "workflow run {run_id} completed after cancellation was requested; cleanup-aware cancellation must return cancel or fail"
682 )));
683 }
684 match self
685 .record_event_at(
686 run_id,
687 snapshot.last_sequence,
688 FlowEvent::RunCompleted { output },
689 )
690 .await
691 {
692 Ok(_) => {}
693 Err(err) if is_event_conflict(&err) => continue,
694 Err(err) => return Err(err),
695 }
696 return self.snapshot(run_id).await;
697 }
698 RuntimeCommand::Fail { error } => {
699 match self
700 .record_event_at(
701 run_id,
702 snapshot.last_sequence,
703 FlowEvent::RunFailed { error },
704 )
705 .await
706 {
707 Ok(_) => {}
708 Err(err) if is_event_conflict(&err) => continue,
709 Err(err) => return Err(err),
710 }
711 return self.snapshot(run_id).await;
712 }
713 RuntimeCommand::Cancel => {
714 let cancellation = snapshot.cancellation.as_ref().ok_or_else(|| {
715 FlowError::InvalidTransition(format!(
716 "workflow run {run_id} returned cancel without a durable cancellation request"
717 ))
718 })?;
719 match self
720 .record_event_at(
721 run_id,
722 snapshot.last_sequence,
723 FlowEvent::RunCancelled {
724 reason: cancellation.request.reason.clone(),
725 },
726 )
727 .await
728 {
729 Ok(_) => {}
730 Err(err) if is_event_conflict(&err) => continue,
731 Err(err) => return Err(err),
732 }
733 return self.snapshot(run_id).await;
734 }
735 RuntimeCommand::Timeout { deadline, reason } => {
736 match self
737 .record_event_at(
738 run_id,
739 snapshot.last_sequence,
740 FlowEvent::RunTimedOut { deadline, reason },
741 )
742 .await
743 {
744 Ok(_) => {}
745 Err(err) if is_event_conflict(&err) => continue,
746 Err(err) => return Err(err),
747 }
748 return self.snapshot(run_id).await;
749 }
750 RuntimeCommand::RecordProgress { progress } => {
751 progress.validate()?;
752 if let Some(existing) = snapshot.progress(&progress.progress_id) {
753 ensure_progress_matches(run_id, existing, &progress)?;
754 return Err(FlowError::InvalidTransition(format!(
755 "workflow rescheduled progress {} without progress",
756 progress.progress_id
757 )));
758 }
759 match self
760 .record_event_at(
761 run_id,
762 snapshot.last_sequence,
763 FlowEvent::RunProgressRecorded { progress },
764 )
765 .await
766 {
767 Ok(_) => {}
768 Err(err) if is_event_conflict(&err) => continue,
769 Err(err) => return Err(err),
770 }
771 }
772 RuntimeCommand::LinkChildOperation { child } => {
773 child.validate()?;
774 if let Some(existing) = snapshot.child_operation(&child.reference_id) {
775 ensure_child_operation_matches(run_id, existing, &child)?;
776 return Err(FlowError::InvalidTransition(format!(
777 "workflow rescheduled child operation {} without progress",
778 child.reference_id
779 )));
780 }
781 match self
782 .record_event_at(
783 run_id,
784 snapshot.last_sequence,
785 FlowEvent::ChildOperationLinked { child },
786 )
787 .await
788 {
789 Ok(_) => {}
790 Err(err) if is_event_conflict(&err) => continue,
791 Err(err) => return Err(err),
792 }
793 }
794 RuntimeCommand::ScheduleStep {
795 step_id,
796 step_name,
797 input,
798 retry,
799 } => {
800 if let Some(step) = snapshot.steps.get(&step_id) {
801 ensure_step_command_matches(run_id, step, &step_name, &input, retry)?;
802 if matches!(
803 step.status,
804 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
805 ) {
806 return Err(FlowError::InvalidTransition(format!(
807 "workflow rescheduled terminal step {step_id} without progress"
808 )));
809 }
810 }
811 ensure_retry_policy_valid(retry)?;
812 match self
813 .execute_step(
814 run_id,
815 &snapshot,
816 StepExecutionContext {
817 step_id,
818 step_name,
819 input,
820 retry,
821 now,
822 },
823 )
824 .await
825 {
826 Ok(()) => {}
827 Err(err) if is_event_conflict(&err) => continue,
828 Err(err) => return Err(err),
829 }
830 }
831 RuntimeCommand::ScheduleSteps { steps } => {
832 ensure_step_batch_valid(&steps)?;
833 for step in &steps {
834 if let Some(existing) = snapshot.steps.get(&step.step_id) {
835 ensure_step_command_matches(
836 run_id,
837 existing,
838 &step.step_name,
839 &step.input,
840 step.retry,
841 )?;
842 }
843 }
844 if steps.iter().all(|step| {
845 snapshot.steps.get(&step.step_id).is_some_and(|existing| {
846 matches!(
847 existing.status,
848 StepStatus::Completed | StepStatus::Failed | StepStatus::Cancelled
849 )
850 })
851 }) {
852 let step_ids = steps
853 .iter()
854 .map(|step| step.step_id.as_str())
855 .collect::<Vec<_>>()
856 .join(", ");
857 return Err(FlowError::InvalidTransition(format!(
858 "workflow rescheduled only terminal steps without progress: {step_ids}"
859 )));
860 }
861 for step in &steps {
862 ensure_retry_policy_valid(step.retry)?;
863 }
864 match self.execute_step_batch(run_id, &snapshot, steps, now).await {
865 Ok(()) => {}
866 Err(err) if is_event_conflict(&err) => continue 'replay,
867 Err(err) => return Err(err),
868 }
869 }
870 RuntimeCommand::WaitUntil { wait_id, resume_at } => {
871 match snapshot.waits.get(&wait_id) {
872 Some(wait) => {
873 ensure_wait_command_matches(run_id, wait, resume_at)?;
874 match wait.status {
875 WaitStatus::Completed => continue,
876 WaitStatus::Waiting => return self.snapshot(run_id).await,
877 WaitStatus::Cancelled => {
878 return Err(FlowError::InvalidTransition(format!(
879 "workflow rescheduled cancelled wait {wait_id}; cancellation cleanup must use a distinct stable identity"
880 )))
881 }
882 }
883 }
884 None => {
885 match self
886 .record_event_at(
887 run_id,
888 snapshot.last_sequence,
889 FlowEvent::WaitCreated { wait_id, resume_at },
890 )
891 .await
892 {
893 Ok(_) => {}
894 Err(err) if is_event_conflict(&err) => continue,
895 Err(err) => return Err(err),
896 }
897 return self.snapshot(run_id).await;
898 }
899 }
900 }
901 RuntimeCommand::CreateHook {
902 hook_id,
903 token,
904 metadata,
905 } => match snapshot.hooks.get(&hook_id) {
906 Some(hook) => {
907 ensure_hook_command_matches(run_id, hook, &token, &metadata)?;
908 match hook.status {
909 HookStatus::Received | HookStatus::Disposed => continue,
910 HookStatus::Active => return self.snapshot(run_id).await,
911 HookStatus::Cancelled => {
912 return Err(FlowError::InvalidTransition(format!(
913 "workflow rescheduled cancelled hook {hook_id}; cancellation cleanup must use a distinct stable identity"
914 )))
915 }
916 }
917 }
918 None => {
919 self.ensure_hook_token_available(run_id, &hook_id, &token)
920 .await?;
921 match self
922 .record_event_at(
923 run_id,
924 snapshot.last_sequence,
925 FlowEvent::HookCreated {
926 hook_id,
927 token,
928 metadata,
929 },
930 )
931 .await
932 {
933 Ok(_) => {}
934 Err(err) if is_event_conflict(&err) => continue,
935 Err(err) => return Err(err),
936 }
937 return self.snapshot(run_id).await;
938 }
939 },
940 }
941 }
942
943 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
944 }
945
946 async fn terminate_run(&self, run_id: &str, event: FlowEvent) -> Result<()> {
947 for _ in 0..self.max_replay_iterations {
948 let snapshot = self.snapshot(run_id).await?;
949 if snapshot.status.is_terminal() {
950 return Ok(());
951 }
952 match self
953 .record_event_at(run_id, snapshot.last_sequence, event.clone())
954 .await
955 {
956 Ok(_) => return Ok(()),
957 Err(err) if is_event_conflict(&err) => continue,
958 Err(err) => return Err(err),
959 }
960 }
961 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
962 }
963
964 async fn record_event_at(
965 &self,
966 run_id: &str,
967 expected_sequence: u64,
968 event: FlowEvent,
969 ) -> Result<FlowEventEnvelope> {
970 let envelope = self
971 .store
972 .append_if_sequence(run_id, expected_sequence, event)
973 .await?;
974 self.observer.observe(envelope.clone()).await;
975 Ok(envelope)
976 }
977
978 async fn ensure_hook_token_available(
979 &self,
980 run_id: &str,
981 hook_id: &str,
982 token: &str,
983 ) -> Result<()> {
984 for active in self.store.find_active_hooks_by_token(token).await? {
985 if active.run_id == run_id && active.hook.hook_id == hook_id {
986 continue;
987 }
988 return Err(FlowError::HookTokenConflict {
989 token: token.to_string(),
990 existing_run_id: active.run_id,
991 existing_hook_id: active.hook.hook_id,
992 });
993 }
994 Ok(())
995 }
996}
997
998fn resolve_scheduled_wakeup(
999 snapshot: &WorkflowRunSnapshot,
1000 wakeup: &ScheduledWakeup,
1001 now: DateTime<Utc>,
1002) -> Option<WorkflowRunSuspension> {
1003 if snapshot.run_id != wakeup.run_id || snapshot.status.is_terminal() {
1004 return None;
1005 }
1006 match wakeup.kind {
1007 ScheduledWakeupKind::Wait => {
1008 let wait = snapshot.waits.get(&wakeup.subject_id)?;
1009 if wait.status != WaitStatus::Waiting || wait.resume_at != wakeup.scheduled_at {
1010 return None;
1011 }
1012 Some(WorkflowRunSuspension::Wait {
1013 run_id: wakeup.run_id.clone(),
1014 wait: wait.clone(),
1015 due: wakeup.scheduled_at <= now,
1016 })
1017 }
1018 ScheduledWakeupKind::Retry => {
1019 let step = snapshot.steps.get(&wakeup.subject_id)?;
1020 if step.status != StepStatus::Pending || step.retry_after != Some(wakeup.scheduled_at) {
1021 return None;
1022 }
1023 Some(WorkflowRunSuspension::Retry {
1024 run_id: wakeup.run_id.clone(),
1025 step: step.clone(),
1026 due: wakeup.scheduled_at <= now,
1027 })
1028 }
1029 }
1030}