Skip to main content

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                concurrency_key: None,
239                priority: 0,
240                concurrency_limits: Vec::new(),
241                max_cost_usd: None,
242                worker_tags: Vec::new(),
243            })
244            .await
245            .expect("create run")
246            .into_run();
247        store
248            .update_run_status(run.id, RunStatus::Running)
249            .await
250            .expect("to running");
251        store
252            .update_run(
253                run.id,
254                RunUpdate {
255                    status: Some(RunStatus::Sleeping),
256                    scheduled_at: Some(scheduled_at),
257                    ..RunUpdate::default()
258                },
259            )
260            .await
261            .expect("to sleeping");
262
263        (store, Waker::new(engine), run.id)
264    }
265
266    #[tokio::test]
267    async fn waker_tick_requeues_due_sleeping_run() {
268        let (store, waker, run_id) = sleeping_run(Utc::now() - TimeDelta::seconds(1)).await;
269
270        waker.tick().await;
271
272        let run = store.get_run(run_id).await.unwrap().unwrap();
273        assert_eq!(run.status.state, RunStatus::Pending);
274        assert!(run.scheduled_at.is_none());
275    }
276
277    #[tokio::test]
278    async fn waker_tick_leaves_future_sleeping_run() {
279        let (store, waker, run_id) = sleeping_run(Utc::now() + TimeDelta::hours(1)).await;
280
281        waker.tick().await;
282
283        let run = store.get_run(run_id).await.unwrap().unwrap();
284        assert_eq!(run.status.state, RunStatus::Sleeping);
285        assert!(run.scheduled_at.is_some());
286    }
287
288    #[tokio::test]
289    async fn waker_run_stops_on_shutdown() {
290        let (_store, waker, _run_id) = sleeping_run(Utc::now()).await;
291        let shutdown = CancellationToken::new();
292        shutdown.cancel();
293
294        // Returns instead of looping forever.
295        timeout(Duration::from_secs(5), waker.run(shutdown))
296            .await
297            .expect("waker stopped");
298    }
299}