Skip to main content

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}