a3s_flow/engine/
continuation.rs1use std::collections::BTreeSet;
2
3use chrono::{DateTime, Utc};
4
5use crate::error::{FlowError, Result};
6use crate::model::{validate_run_id, WorkflowRunSnapshot, WorkflowSpec};
7
8use super::validation::ensure_same_start;
9use super::FlowEngine;
10
11impl FlowEngine {
12 pub async fn continuation_chain(&self, run_id: &str) -> Result<Vec<WorkflowRunSnapshot>> {
18 validate_run_id(run_id)?;
19 let mut current_run_id = run_id.to_string();
20 let mut visited = BTreeSet::new();
21 let mut chain = Vec::new();
22 let mut expected_start: Option<(WorkflowSpec, serde_json::Value)> = None;
23
24 for hop in 0..=self.max_continue_as_new_hops {
25 if !visited.insert(current_run_id.clone()) {
26 return Err(FlowError::ContinueAsNewCycle(current_run_id));
27 }
28 let snapshot = self.snapshot(¤t_run_id).await?;
29 if let Some((expected_spec, expected_input)) = expected_start.as_ref() {
30 ensure_same_start(¤t_run_id, &snapshot, expected_spec, expected_input)?;
31 }
32 let successor_run_id = snapshot
33 .continuation
34 .as_ref()
35 .map(|continuation| continuation.successor_run_id.clone());
36 expected_start = snapshot
37 .continuation
38 .as_ref()
39 .map(|continuation| (snapshot.spec.clone(), continuation.input.clone()));
40 chain.push(snapshot);
41 let Some(successor_run_id) = successor_run_id else {
42 return Ok(chain);
43 };
44 if hop == self.max_continue_as_new_hops {
45 return Err(FlowError::ContinueAsNewLimitExceeded(
46 self.max_continue_as_new_hops,
47 ));
48 }
49 current_run_id = successor_run_id;
50 }
51
52 Err(FlowError::ContinueAsNewLimitExceeded(
53 self.max_continue_as_new_hops,
54 ))
55 }
56
57 pub async fn drive(&self, run_id: &str) -> Result<WorkflowRunSnapshot> {
64 self.drive_at(run_id, Utc::now()).await
65 }
66
67 pub(super) async fn drive_at(
68 &self,
69 run_id: &str,
70 now: DateTime<Utc>,
71 ) -> Result<WorkflowRunSnapshot> {
72 self.drive_at_with_child_context(run_id, now, 0, &BTreeSet::new())
73 .await
74 }
75
76 pub(super) async fn recover_and_drive_continuation_leaf(
80 &self,
81 run_id: &str,
82 ) -> Result<WorkflowRunSnapshot> {
83 self.recover_and_drive_continuation_leaf_at(run_id, Utc::now())
84 .await
85 }
86
87 pub(super) async fn recover_and_drive_continuation_leaf_at(
88 &self,
89 run_id: &str,
90 now: DateTime<Utc>,
91 ) -> Result<WorkflowRunSnapshot> {
92 let leaf = self.ensure_continuation_leaf(run_id).await?;
93 if leaf.status.is_terminal() {
94 return Ok(leaf);
95 }
96 self.drive_at(&leaf.run_id, now).await
97 }
98
99 pub(super) async fn drive_at_with_child_context(
100 &self,
101 run_id: &str,
102 now: DateTime<Utc>,
103 child_depth: usize,
104 ancestors: &BTreeSet<String>,
105 ) -> Result<WorkflowRunSnapshot> {
106 let mut current_run_id = run_id.to_string();
107 let mut visited = BTreeSet::new();
108
109 for hop in 0..=self.max_continue_as_new_hops {
110 if ancestors.contains(¤t_run_id) {
111 return Err(FlowError::ChildWorkflowCycle(current_run_id));
112 }
113 if !visited.insert(current_run_id.clone()) {
114 return Err(FlowError::ContinueAsNewCycle(current_run_id));
115 }
116
117 let allow_continue_as_new = hop < self.max_continue_as_new_hops;
118 let mut active_ancestry = ancestors.clone();
119 active_ancestry.extend(visited.iter().cloned());
120 let snapshot = self
121 .drive_run_at(
122 ¤t_run_id,
123 now,
124 allow_continue_as_new,
125 child_depth,
126 &active_ancestry,
127 )
128 .await?;
129 let Some(successor_run_id) = self
130 .ensure_continuation_successor(&snapshot, &visited, hop)
131 .await?
132 else {
133 return Ok(snapshot);
134 };
135 current_run_id = successor_run_id;
136 }
137
138 Err(FlowError::ContinueAsNewLimitExceeded(
139 self.max_continue_as_new_hops,
140 ))
141 }
142
143 pub(super) async fn ensure_continuation_leaf(
144 &self,
145 run_id: &str,
146 ) -> Result<WorkflowRunSnapshot> {
147 validate_run_id(run_id)?;
148 let mut current_run_id = run_id.to_string();
149 let mut visited = BTreeSet::new();
150
151 for hop in 0..=self.max_continue_as_new_hops {
152 if !visited.insert(current_run_id.clone()) {
153 return Err(FlowError::ContinueAsNewCycle(current_run_id));
154 }
155 let snapshot = self.snapshot(¤t_run_id).await?;
156 let Some(successor_run_id) = self
157 .ensure_continuation_successor(&snapshot, &visited, hop)
158 .await?
159 else {
160 return Ok(snapshot);
161 };
162 current_run_id = successor_run_id;
163 }
164
165 Err(FlowError::ContinueAsNewLimitExceeded(
166 self.max_continue_as_new_hops,
167 ))
168 }
169
170 async fn ensure_continuation_successor(
171 &self,
172 snapshot: &WorkflowRunSnapshot,
173 visited: &BTreeSet<String>,
174 hop: usize,
175 ) -> Result<Option<String>> {
176 let Some(continuation) = snapshot.continuation.as_ref() else {
177 return Ok(None);
178 };
179 if hop == self.max_continue_as_new_hops {
180 return Err(FlowError::ContinueAsNewLimitExceeded(
181 self.max_continue_as_new_hops,
182 ));
183 }
184 if visited.contains(&continuation.successor_run_id) {
185 return Err(FlowError::ContinueAsNewCycle(
186 continuation.successor_run_id.clone(),
187 ));
188 }
189 self.ensure_run_started_with_admission(
193 &continuation.successor_run_id,
194 &snapshot.spec,
195 &continuation.input,
196 false,
197 )
198 .await?;
199 Ok(Some(continuation.successor_run_id.clone()))
200 }
201}