ironflow_api/waker.rs
1//! Wake-up of runs paused in `Sleeping`.
2//!
3//! A run sleeps after a `ctx.delay` step, or while `ctx.wait_for_signal` waits
4//! for a signal. Its wake-up time (`scheduled_at`) lives on the run row, so it
5//! survives an API or worker restart. The [`Waker`] periodically hands every due
6//! run to the engine's [`RunWaker`], which moves it back to `Pending` and, when
7//! the API has no worker, resumes it in-process.
8//!
9//! A run wakes exactly once even when several API instances run this loop.
10
11use std::sync::Arc;
12use std::time::Duration;
13
14use ironflow_engine::engine::Engine;
15use ironflow_engine::wake::RunWaker;
16use tokio::select;
17use tokio::time::interval;
18use tokio_util::sync::CancellationToken;
19use tracing::{error, info};
20
21/// How often due sleeping runs are collected.
22///
23/// Also the worst-case lag between a delay elapsing (or a signal deadline
24/// passing) and the run resuming.
25pub const DEFAULT_WAKER_INTERVAL: Duration = Duration::from_secs(10);
26
27/// How many runs a single tick wakes.
28///
29/// Bounded so that a burst of due runs (after a long outage) resorbs
30/// progressively instead of holding a long transaction on the runs table.
31pub const DEFAULT_WAKER_BATCH_SIZE: u32 = 50;
32
33/// Periodic task that wakes `Sleeping` runs whose `scheduled_at` has passed.
34///
35/// # Examples
36///
37/// ```no_run
38/// use std::sync::Arc;
39/// use std::time::Duration;
40/// use ironflow_api::waker::Waker;
41/// use ironflow_core::providers::claude::ClaudeCodeProvider;
42/// use ironflow_engine::engine::Engine;
43/// use ironflow_store::memory::InMemoryStore;
44/// use ironflow_store::store::Store;
45/// use tokio_util::sync::CancellationToken;
46///
47/// # async fn example() {
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 = Waker::new(engine).interval(Duration::from_secs(5));
52/// tokio::spawn(waker.run(CancellationToken::new()));
53/// # }
54/// ```
55pub struct Waker {
56 waker: RunWaker,
57 interval: Duration,
58}
59
60impl Waker {
61 /// Create a waker with the default interval and batch size.
62 ///
63 /// # Examples
64 ///
65 /// ```no_run
66 /// use std::sync::Arc;
67 /// use ironflow_api::waker::Waker;
68 /// use ironflow_core::providers::claude::ClaudeCodeProvider;
69 /// use ironflow_engine::engine::Engine;
70 /// use ironflow_store::memory::InMemoryStore;
71 /// use ironflow_store::store::Store;
72 ///
73 /// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
74 /// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
75 /// let waker = Waker::new(engine);
76 /// ```
77 pub fn new(engine: Arc<Engine>) -> Self {
78 Self {
79 waker: RunWaker::new(engine).batch_size(DEFAULT_WAKER_BATCH_SIZE),
80 interval: DEFAULT_WAKER_INTERVAL,
81 }
82 }
83
84 /// Set how often due runs are collected.
85 ///
86 /// # Examples
87 ///
88 /// ```no_run
89 /// use std::sync::Arc;
90 /// use std::time::Duration;
91 /// use ironflow_api::waker::Waker;
92 /// use ironflow_core::providers::claude::ClaudeCodeProvider;
93 /// use ironflow_engine::engine::Engine;
94 /// use ironflow_store::memory::InMemoryStore;
95 /// use ironflow_store::store::Store;
96 ///
97 /// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
98 /// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
99 /// let waker = Waker::new(engine).interval(Duration::from_secs(5));
100 /// ```
101 pub fn interval(mut self, interval: Duration) -> Self {
102 self.interval = interval;
103 self
104 }
105
106 /// Set how many runs a single tick wakes.
107 ///
108 /// # Examples
109 ///
110 /// ```no_run
111 /// use std::sync::Arc;
112 /// use ironflow_api::waker::Waker;
113 /// use ironflow_core::providers::claude::ClaudeCodeProvider;
114 /// use ironflow_engine::engine::Engine;
115 /// use ironflow_store::memory::InMemoryStore;
116 /// use ironflow_store::store::Store;
117 ///
118 /// let store: Arc<dyn Store> = Arc::new(InMemoryStore::new());
119 /// let engine = Arc::new(Engine::new(store, Arc::new(ClaudeCodeProvider::new())));
120 /// let waker = Waker::new(engine).batch_size(10);
121 /// ```
122 pub fn batch_size(self, batch_size: u32) -> Self {
123 Self {
124 waker: self.waker.batch_size(batch_size),
125 interval: self.interval,
126 }
127 }
128
129 /// Run the wake-up loop until `shutdown` is cancelled.
130 ///
131 /// Store errors are logged and the loop keeps going: a transient database
132 /// failure must not leave runs asleep forever.
133 ///
134 /// # Examples
135 ///
136 /// ```no_run
137 /// use ironflow_api::waker::Waker;
138 /// use tokio_util::sync::CancellationToken;
139 ///
140 /// # async fn example(waker: Waker) {
141 /// let shutdown = CancellationToken::new();
142 /// tokio::spawn(waker.run(shutdown.clone()));
143 /// shutdown.cancel();
144 /// # }
145 /// ```
146 pub async fn run(self, shutdown: CancellationToken) {
147 let mut ticker = interval(self.interval);
148 // The first tick fires immediately; skip it so startup is not a burst.
149 ticker.tick().await;
150
151 info!(
152 interval_secs = self.interval.as_secs(),
153 "sleeping run waker started"
154 );
155
156 loop {
157 select! {
158 _ = shutdown.cancelled() => {
159 info!("sleeping run waker stopped");
160 return;
161 }
162 _ = ticker.tick() => {
163 self.tick().await;
164 }
165 }
166 }
167 }
168
169 /// Wake one batch of due runs.
170 ///
171 /// Exposed for tests and for callers that drive the schedule themselves.
172 ///
173 /// # Examples
174 ///
175 /// ```no_run
176 /// use ironflow_api::waker::Waker;
177 ///
178 /// # async fn example(waker: Waker) {
179 /// waker.tick().await;
180 /// # }
181 /// ```
182 pub async fn tick(&self) {
183 let runs = match self.waker.tick().await {
184 Ok(runs) => runs,
185 Err(err) => {
186 error!(error = %err, "failed to wake due sleeping runs");
187 return;
188 }
189 };
190
191 for run in &runs {
192 info!(
193 run_id = %run.id,
194 workflow = %run.workflow_name,
195 "sleeping run requeued"
196 );
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use std::collections::HashMap;
204
205 use chrono::{DateTime, TimeDelta, Utc};
206 use ironflow_core::providers::claude::ClaudeCodeProvider;
207 use ironflow_engine::engine::ExecutionMode;
208 use ironflow_store::entities::{NewRun, RunStatus, RunUpdate, TriggerKind};
209 use ironflow_store::memory::InMemoryStore;
210 use ironflow_store::store::{RunStore, Store};
211 use serde_json::json;
212 use tokio::time::timeout;
213 use uuid::Uuid;
214
215 use super::*;
216
217 /// Build a waker (workers mode: woken runs stay `Pending`) over a store
218 /// holding one run sleeping until `scheduled_at`.
219 async fn sleeping_run(scheduled_at: DateTime<Utc>) -> (Arc<InMemoryStore>, Waker, Uuid) {
220 let store = Arc::new(InMemoryStore::new());
221 let store_dyn: Arc<dyn Store> = store.clone();
222 let engine = Arc::new(
223 Engine::new(store_dyn, Arc::new(ClaudeCodeProvider::new()))
224 .with_execution_mode(ExecutionMode::Workers),
225 );
226
227 let run = store
228 .create_run(NewRun {
229 created_by: None,
230 workflow_name: "deploy".to_string(),
231 trigger: TriggerKind::Manual,
232 payload: json!({}),
233 max_retries: 0,
234 handler_version: None,
235 labels: HashMap::new(),
236 scheduled_at: None,
237 idempotency_key: None,
238 max_cost_usd: None,
239 })
240 .await
241 .expect("create run")
242 .into_run();
243 store
244 .update_run_status(run.id, RunStatus::Running)
245 .await
246 .expect("to running");
247 store
248 .update_run(
249 run.id,
250 RunUpdate {
251 status: Some(RunStatus::Sleeping),
252 scheduled_at: Some(scheduled_at),
253 ..RunUpdate::default()
254 },
255 )
256 .await
257 .expect("to sleeping");
258
259 (store, Waker::new(engine), run.id)
260 }
261
262 #[tokio::test]
263 async fn waker_tick_requeues_due_sleeping_run() {
264 let (store, waker, run_id) = sleeping_run(Utc::now() - TimeDelta::seconds(1)).await;
265
266 waker.tick().await;
267
268 let run = store.get_run(run_id).await.unwrap().unwrap();
269 assert_eq!(run.status.state, RunStatus::Pending);
270 assert!(run.scheduled_at.is_none());
271 }
272
273 #[tokio::test]
274 async fn waker_tick_leaves_future_sleeping_run() {
275 let (store, waker, run_id) = sleeping_run(Utc::now() + TimeDelta::hours(1)).await;
276
277 waker.tick().await;
278
279 let run = store.get_run(run_id).await.unwrap().unwrap();
280 assert_eq!(run.status.state, RunStatus::Sleeping);
281 assert!(run.scheduled_at.is_some());
282 }
283
284 #[tokio::test]
285 async fn waker_run_stops_on_shutdown() {
286 let (_store, waker, _run_id) = sleeping_run(Utc::now()).await;
287 let shutdown = CancellationToken::new();
288 shutdown.cancel();
289
290 // Returns instead of looping forever.
291 timeout(Duration::from_secs(5), waker.run(shutdown))
292 .await
293 .expect("waker stopped");
294 }
295}