Skip to main content

a3s_flow/engine/
scheduling.rs

1use chrono::{DateTime, Utc};
2
3use crate::error::{FlowError, Result};
4use crate::model::{project_run, FlowEvent, ScheduledWakeup, ScheduledWakeupKind, WaitStatus};
5use crate::store::scheduled_wakeups_for_snapshot;
6
7use super::{
8    validation::is_event_conflict, FlowEngine, ScheduledRunOutcome, WaitResolutionOutcome,
9};
10
11impl FlowEngine {
12    /// Resume a wait once its timer has fired.
13    ///
14    /// Redelivery is idempotent after the existing wait has completed or its
15    /// run has become terminal. A resolved wait still drives recovery through
16    /// any committed continue-as-new boundary, but no second `wait_completed`
17    /// event is appended.
18    pub async fn resume_wait(&self, run_id: &str, wait_id: &str) -> Result<()> {
19        self.resume_wait_if_open(run_id, wait_id).await?;
20        Ok(())
21    }
22
23    /// Resume `wait_id` and report the driven leaf plus completion ownership.
24    pub(crate) async fn resume_wait_if_open(
25        &self,
26        run_id: &str,
27        wait_id: &str,
28    ) -> Result<WaitResolutionOutcome> {
29        self.resume_wait_if_open_at(run_id, wait_id, Utc::now())
30            .await
31    }
32
33    async fn resume_wait_if_open_at(
34        &self,
35        run_id: &str,
36        wait_id: &str,
37        now: DateTime<Utc>,
38    ) -> Result<WaitResolutionOutcome> {
39        let mut resumed = false;
40        for _ in 0..self.max_replay_iterations {
41            let snapshot = self.snapshot(run_id).await?;
42            let Some(wait) = snapshot.waits.get(wait_id) else {
43                if snapshot.status.is_terminal() {
44                    return Err(FlowError::RunTerminal(run_id.to_string()));
45                }
46                return Err(FlowError::InvalidTransition(format!(
47                    "wait {wait_id} does not exist for run {run_id}"
48                )));
49            };
50            if snapshot.status.is_terminal() {
51                match self
52                    .recover_and_drive_continuation_leaf_at(run_id, now)
53                    .await
54                {
55                    Ok(snapshot) => return Ok(wait_resolution(run_id, wait_id, snapshot, resumed)),
56                    Err(error) if is_event_conflict(&error) => continue,
57                    Err(error) => return Err(error),
58                }
59            }
60
61            match wait.status {
62                WaitStatus::Waiting => {
63                    self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
64                    match self
65                        .record_event_at(
66                            run_id,
67                            snapshot.last_sequence,
68                            FlowEvent::WaitCompleted {
69                                wait_id: wait_id.to_string(),
70                            },
71                        )
72                        .await
73                    {
74                        Ok(_) => resumed = true,
75                        Err(error) if is_event_conflict(&error) => continue,
76                        Err(error) => return Err(error),
77                    }
78                }
79                WaitStatus::Completed | WaitStatus::Cancelled => {
80                    self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
81                }
82            }
83
84            match self
85                .recover_and_drive_continuation_leaf_at(run_id, now)
86                .await
87            {
88                Ok(snapshot) => return Ok(wait_resolution(run_id, wait_id, snapshot, resumed)),
89                Err(error) if is_event_conflict(&error) => continue,
90                Err(error) => return Err(error),
91            }
92        }
93
94        Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
95    }
96
97    /// List active waits whose `resume_at` is at or before `now`.
98    ///
99    /// Scheduler integrations can use this to inspect due timers before
100    /// deciding how aggressively to drive them.
101    pub async fn list_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
102        let mut due = self
103            .list_due_wakeups(now)
104            .await?
105            .into_iter()
106            .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Wait)
107            .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
108            .collect::<Vec<_>>();
109        due.sort();
110        Ok(due)
111    }
112
113    /// Complete every due wait and drive the affected workflows.
114    ///
115    /// Returns only the `(run_id, wait_id)` pairs completed by this call. A
116    /// wait completed or cancelled by another caller after the due scan is
117    /// safely skipped.
118    pub async fn resume_due_waits(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
119        let due = self.list_due_waits(now).await?;
120        let mut resumed = Vec::with_capacity(due.len());
121        for (run_id, wait_id) in due {
122            let resolution = self.resume_wait_if_open_at(&run_id, &wait_id, now).await?;
123            if resolution.committed {
124                resumed.push((resolution.wait_run_id, resolution.wait_id));
125            }
126        }
127        Ok(resumed)
128    }
129
130    /// List pending step retries whose `retry_after` is at or before `now`.
131    pub async fn list_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
132        let mut due = self
133            .list_due_wakeups(now)
134            .await?
135            .into_iter()
136            .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Retry)
137            .map(|wakeup| (wakeup.run_id, wakeup.subject_id))
138            .collect::<Vec<_>>();
139        due.sort();
140        Ok(due)
141    }
142
143    /// List all due wait timers and delayed retries through the store boundary.
144    pub async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
145        let mut wakeups = self.store.list_due_wakeups(now).await?;
146        wakeups.sort_by(|left, right| {
147            (left.kind, left.run_id.as_str(), left.subject_id.as_str()).cmp(&(
148                right.kind,
149                right.run_id.as_str(),
150                right.subject_id.as_str(),
151            ))
152        });
153        Ok(wakeups)
154    }
155
156    /// Drive every run with a due step retry.
157    pub async fn resume_due_retries(&self, now: DateTime<Utc>) -> Result<Vec<(String, String)>> {
158        let due = self.list_due_retries(now).await?;
159        let mut run_ids = Vec::new();
160        for (run_id, _) in &due {
161            if !run_ids.contains(run_id) {
162                run_ids.push(run_id.clone());
163            }
164        }
165        for run_id in run_ids {
166            self.drive_at(&run_id, now).await?;
167        }
168        Ok(due)
169    }
170
171    /// Resume the due waits and delayed retries for one targeted run.
172    ///
173    /// Unlike the compatibility-wide `resume_due_*` methods, this path loads
174    /// only `run_id` and never performs another global due-wakeup query. The
175    /// returned records describe the wakeups that were still due when the task
176    /// began handling.
177    pub async fn resume_scheduled_run(
178        &self,
179        run_id: &str,
180        now: DateTime<Utc>,
181    ) -> Result<Vec<ScheduledWakeup>> {
182        let outcome = self
183            .resume_scheduled_run_with_committed_waits(run_id, now)
184            .await?;
185        Ok(outcome.due)
186    }
187
188    /// Resume one scheduled run and report wait completions committed here.
189    pub(crate) async fn resume_scheduled_run_with_committed_waits(
190        &self,
191        run_id: &str,
192        now: DateTime<Utc>,
193    ) -> Result<ScheduledRunOutcome> {
194        let history = self.store.list(run_id).await?;
195        let snapshot = project_run(run_id, &history)?;
196        if snapshot.status.is_terminal() {
197            let snapshot = self
198                .recover_and_drive_continuation_leaf_at(run_id, now)
199                .await?;
200            return Ok(ScheduledRunOutcome {
201                snapshot,
202                due: Vec::new(),
203                resumed_waits: Vec::new(),
204            });
205        }
206        self.ensure_runtime_build_available(run_id, &snapshot.spec)?;
207        let due = scheduled_wakeups_for_snapshot(&snapshot)
208            .into_iter()
209            .filter(|wakeup| wakeup.scheduled_at <= now)
210            .collect::<Vec<_>>();
211
212        let due_wait_ids = due
213            .iter()
214            .filter(|wakeup| wakeup.kind == ScheduledWakeupKind::Wait)
215            .map(|wakeup| wakeup.subject_id.clone())
216            .collect::<Vec<_>>();
217        let has_due_retries = due
218            .iter()
219            .any(|wakeup| wakeup.kind == ScheduledWakeupKind::Retry);
220
221        let mut resumed_waits = Vec::with_capacity(due_wait_ids.len());
222        let mut driven_snapshot = snapshot;
223        for wait_id in due_wait_ids {
224            let resolution = self.resume_wait_if_open_at(run_id, &wait_id, now).await?;
225            driven_snapshot = resolution.snapshot;
226            if resolution.committed {
227                resumed_waits.push((resolution.wait_run_id, resolution.wait_id));
228            }
229        }
230        if has_due_retries {
231            driven_snapshot = self
232                .recover_and_drive_continuation_leaf_at(run_id, now)
233                .await?;
234        }
235
236        Ok(ScheduledRunOutcome {
237            snapshot: driven_snapshot,
238            due,
239            resumed_waits,
240        })
241    }
242}
243
244fn wait_resolution(
245    run_id: &str,
246    wait_id: &str,
247    snapshot: crate::WorkflowRunSnapshot,
248    committed: bool,
249) -> WaitResolutionOutcome {
250    WaitResolutionOutcome {
251        wait_run_id: run_id.to_string(),
252        wait_id: wait_id.to_string(),
253        snapshot,
254        committed,
255    }
256}