ironflow_engine/wake.rs
1//! [`RunWaker`] -- wakes `Sleeping` runs whose `scheduled_at` has passed.
2//!
3//! A run goes `Sleeping` when a [`delay`](crate::context::WorkflowContext::delay)
4//! step pauses it, or when
5//! [`wait_for_signal`](crate::context::WorkflowContext::wait_for_signal) suspends
6//! it until a signal or its deadline. Either way the wake-up time lives on the
7//! run itself (`scheduled_at`), so the timer survives an API or worker restart:
8//! nothing is held in memory, and a brand-new process picks the run up on its
9//! next tick.
10//!
11//! [`RunWaker::tick`] claims every due run and moves it back to `Pending` in
12//! the same transaction that clears `scheduled_at`, so a run wakes **exactly
13//! once** even with several API instances running a waker. Under
14//! [`ExecutionMode::Workers`] a worker then picks the run up; under
15//! [`ExecutionMode::Local`] the waker resumes it in-process.
16//!
17//! Before claiming, each tick also cancels the root runs left `Sleeping` or
18//! `AwaitingApproval` past their
19//! [`run_deadline`](crate::guard::WorkflowGuardConfig::run_deadline), through
20//! [`Engine::cancel_runs_past_deadline`]: a run past its deadline is stopped
21//! instead of being woken.
22
23use std::sync::Arc;
24
25use tracing::info;
26
27use ironflow_store::models::Run;
28
29use crate::engine::{Engine, ExecutionMode};
30use crate::error::EngineError;
31
32/// How many runs a single [`RunWaker::tick`] wakes.
33pub const DEFAULT_WAKE_BATCH_SIZE: u32 = 50;
34
35/// Wakes `Sleeping` runs whose wake-up time has passed.
36///
37/// # Examples
38///
39/// ```no_run
40/// use std::sync::Arc;
41/// use ironflow_core::providers::claude::ClaudeCodeProvider;
42/// use ironflow_engine::engine::Engine;
43/// use ironflow_engine::wake::RunWaker;
44/// use ironflow_store::memory::InMemoryStore;
45/// use ironflow_store::store::Store;
46///
47/// # async fn example() -> Result<(), ironflow_engine::error::EngineError> {
48/// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
49/// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
50///
51/// let waker = RunWaker::new(engine).batch_size(10);
52/// let woken = waker.tick().await?;
53/// println!("{} runs woken", woken.len());
54/// # Ok(())
55/// # }
56/// ```
57#[derive(Debug, Clone)]
58pub struct RunWaker {
59 engine: Arc<Engine>,
60 batch_size: u32,
61}
62
63impl RunWaker {
64 /// Create a waker with the default batch size.
65 ///
66 /// # Examples
67 ///
68 /// ```no_run
69 /// use std::sync::Arc;
70 /// use ironflow_core::providers::claude::ClaudeCodeProvider;
71 /// use ironflow_engine::engine::Engine;
72 /// use ironflow_engine::wake::RunWaker;
73 /// use ironflow_store::memory::InMemoryStore;
74 /// use ironflow_store::store::Store;
75 ///
76 /// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
77 /// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
78 /// let waker = RunWaker::new(engine);
79 /// ```
80 pub fn new(engine: Arc<Engine>) -> Self {
81 Self {
82 engine,
83 batch_size: DEFAULT_WAKE_BATCH_SIZE,
84 }
85 }
86
87 /// Set how many runs a single [`tick`](Self::tick) wakes.
88 ///
89 /// # Examples
90 ///
91 /// ```no_run
92 /// use std::sync::Arc;
93 /// use ironflow_core::providers::claude::ClaudeCodeProvider;
94 /// use ironflow_engine::engine::Engine;
95 /// use ironflow_engine::wake::RunWaker;
96 /// use ironflow_store::memory::InMemoryStore;
97 /// use ironflow_store::store::Store;
98 ///
99 /// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
100 /// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
101 /// let waker = RunWaker::new(engine).batch_size(10);
102 /// ```
103 pub fn batch_size(mut self, batch_size: u32) -> Self {
104 self.batch_size = batch_size;
105 self
106 }
107
108 /// Claim and wake one batch of due `Sleeping` runs.
109 ///
110 /// First, it cancels up to one batch of root runs left `Sleeping` or
111 /// `AwaitingApproval` past their
112 /// [`run_deadline`](crate::guard::WorkflowGuardConfig::run_deadline)
113 /// (see [`Engine::cancel_runs_past_deadline`]), so they are stopped
114 /// instead of woken.
115 ///
116 /// Every claimed run is `Pending` when this returns. Under
117 /// [`ExecutionMode::Local`] each one is then resumed in a background task,
118 /// so a long workflow never holds the waker loop; a failed resume is
119 /// logged, not rolled back.
120 ///
121 /// Returns the woken runs as they were right after the transition.
122 ///
123 /// # Errors
124 ///
125 /// Returns [`EngineError::Store`] if the runs past their deadline cannot
126 /// be listed or the batch cannot be claimed.
127 ///
128 /// # Examples
129 ///
130 /// ```no_run
131 /// use ironflow_engine::wake::RunWaker;
132 ///
133 /// # use ironflow_engine::error::EngineError;
134 /// # async fn example(waker: &RunWaker) -> Result<(), EngineError> {
135 /// for run in waker.tick().await? {
136 /// println!("woke {}", run.id);
137 /// }
138 /// # Ok(())
139 /// # }
140 /// ```
141 pub async fn tick(&self) -> Result<Vec<Run>, EngineError> {
142 let stopped = self
143 .engine
144 .cancel_runs_past_deadline(self.batch_size)
145 .await?;
146 for run_id in &stopped {
147 info!(run_id = %run_id, "run past its deadline stopped instead of woken");
148 }
149
150 let runs = self
151 .engine
152 .store()
153 .claim_due_sleeping_runs(self.batch_size)
154 .await?;
155
156 for run in &runs {
157 info!(
158 run_id = %run.id,
159 workflow = %run.workflow_name,
160 "sleeping run woken"
161 );
162 if self.engine.execution_mode() == ExecutionMode::Local {
163 self.engine.spawn_local_resume(run.id);
164 }
165 }
166
167 Ok(runs)
168 }
169}