ironflow_engine/context/steps/
signal.rs1use std::time::Duration;
4
5use chrono::{DateTime, TimeDelta, Utc};
6use schemars::schema_for;
7use serde_json::{Value, from_value, json, to_value};
8use tracing::info;
9use uuid::Uuid;
10
11use ironflow_store::models::{
12 NewStep, SignalStepResolution, Step, StepKind, StepStatus, StepUpdate, step_trace_id,
13};
14
15use crate::context::WorkflowContext;
16use crate::context::lifecycle::check_replay_identity;
17use crate::error::EngineError;
18use crate::executor::SignalOutcome;
19use crate::plan::lock_plan;
20use crate::signal::{
21 SIGNAL_SCHEMA_KEY, SIGNAL_TIMED_OUT_KEY, Signal, received_output, timed_out_output,
22};
23
24const DEADLINE_AT_KEY: &str = "deadline_at";
26
27impl WorkflowContext {
28 pub async fn wait_for_signal<S: Signal>(
96 &mut self,
97 name: &str,
98 key: &str,
99 timeout: Duration,
100 ) -> Result<Option<S>, EngineError> {
101 assert!(
102 !S::NAME.is_empty(),
103 "wait_for_signal: Signal::NAME must not be empty"
104 );
105 assert!(
106 !key.trim().is_empty(),
107 "wait_for_signal: key must not be empty"
108 );
109 assert!(
110 !timeout.is_zero(),
111 "wait_for_signal: timeout must be greater than zero"
112 );
113
114 if let Some(plan) = self.plan().cloned() {
116 self.position += 1;
117 {
118 let mut recorder = lock_plan(&plan);
119 recorder.record_or_stop(name, StepKind::Signal, &self.workflow_name, None)?;
120 recorder.set_last(vec![name.to_string()]);
121 }
122 return Ok(from_value::<S>(json!({})).ok());
123 }
124
125 let position = self.next_position();
126
127 if let Some(existing) = self.replay_steps.get(&position).cloned() {
129 check_replay_identity(&existing, position, name, &StepKind::Signal)?;
130 self.last_step_ids = vec![existing.id];
131 return self.signal_replay::<S>(name, key, existing).await;
132 }
133
134 let schema = to_value(schema_for!(S))?;
135 let now = Utc::now();
136 let deadline_at = deadline(name, now, timeout)?;
137 let input = json!({
138 "name": S::NAME,
139 "key": key,
140 SIGNAL_SCHEMA_KEY: schema,
141 "waiting_since": now,
142 DEADLINE_AT_KEY: deadline_at,
143 });
144
145 if let Some(interceptor) = self.interceptor.clone()
147 && let Some(outcome) = interceptor.intercept_signal(name, S::NAME, key, &schema)
148 {
149 let step = self.create_signal_step(name, position, input).await?;
150 self.start_step(step.id, now).await?;
151 self.last_step_ids = vec![step.id];
152
153 let output = match outcome {
154 SignalOutcome::Received(payload) => json!({
155 SIGNAL_TIMED_OUT_KEY: false,
156 "signal_id": null,
157 "payload": payload,
158 }),
159 SignalOutcome::TimedOut => timed_out_output(),
160 };
161 self.store
162 .update_step(
163 step.id,
164 StepUpdate {
165 status: Some(StepStatus::Completed),
166 output: Some(output.clone()),
167 completed_at: Some(Utc::now()),
168 ..StepUpdate::default()
169 },
170 )
171 .await?;
172 info!(
173 run_id = %self.run_id,
174 step = %name,
175 position,
176 "signal step resolved by the step interceptor"
177 );
178 return decode_signal_output(name, &output);
179 }
180
181 let step = self.create_signal_step(name, position, input).await?;
184 self.start_step(step.id, now).await?;
185 self.last_step_ids = vec![step.id];
186
187 if let Some(output) = self.received_signal::<S>(step.id, key).await? {
188 info!(
189 run_id = %self.run_id,
190 step = %name,
191 position,
192 "signal already received, not suspending"
193 );
194 return decode_signal_output(name, &output);
195 }
196
197 info!(
198 run_id = %self.run_id,
199 step = %name,
200 signal = %S::NAME,
201 key = %key,
202 deadline_at = %deadline_at,
203 "waiting for signal"
204 );
205 Err(EngineError::SignalWaiting {
206 run_id: self.run_id,
207 step_id: step.id,
208 step_name: name.to_string(),
209 name: S::NAME.to_string(),
210 key: key.to_string(),
211 deadline_at,
212 })
213 }
214
215 async fn signal_replay<S: Signal>(
217 &mut self,
218 name: &str,
219 key: &str,
220 existing: Step,
221 ) -> Result<Option<S>, EngineError> {
222 match existing.status.state {
223 StepStatus::Completed => {
224 let output = require_output(name, existing.output)?;
225 info!(
226 run_id = %self.run_id,
227 step = %name,
228 "signal step replayed (resolved)"
229 );
230 decode_signal_output(name, &output)
231 }
232 StepStatus::Running => {
233 let deadline_at = stored_deadline(name, existing.input.as_ref())?;
234
235 if Utc::now() >= deadline_at {
236 let resolution = self
237 .store
238 .resolve_signal_step(existing.id, timed_out_output())
239 .await?;
240 let output = match resolution {
241 SignalStepResolution::Resolved { .. } => timed_out_output(),
242 SignalStepResolution::NotWaiting { output } => {
244 require_output(name, output)?
245 }
246 };
247 info!(
248 run_id = %self.run_id,
249 step = %name,
250 "signal step deadline reached"
251 );
252 return decode_signal_output(name, &output);
253 }
254
255 if let Some(output) = self.received_signal::<S>(existing.id, key).await? {
258 return decode_signal_output(name, &output);
259 }
260 info!(
261 run_id = %self.run_id,
262 step = %name,
263 deadline_at = %deadline_at,
264 "signal still not received, suspending again"
265 );
266 Err(EngineError::SignalWaiting {
267 run_id: self.run_id,
268 step_id: existing.id,
269 step_name: name.to_string(),
270 name: S::NAME.to_string(),
271 key: key.to_string(),
272 deadline_at,
273 })
274 }
275 state => Err(EngineError::StepConfig(format!(
276 "signal step '{name}' is in state {state:?}"
277 ))),
278 }
279 }
280
281 async fn received_signal<S: Signal>(
287 &self,
288 step_id: Uuid,
289 key: &str,
290 ) -> Result<Option<Value>, EngineError> {
291 let since = self.run_created_at().await?;
292 let signals = self.store.list_signals_for_key(S::NAME, key, since).await?;
293 let Some(signal) = signals
294 .iter()
295 .find(|s| from_value::<S>(s.payload.clone()).is_ok())
296 else {
297 return Ok(None);
298 };
299
300 let output = received_output(signal);
301 let resolution = self
302 .store
303 .resolve_signal_step(step_id, output.clone())
304 .await?;
305 match resolution {
306 SignalStepResolution::Resolved { .. } => Ok(Some(output)),
307 SignalStepResolution::NotWaiting { output } => Ok(output),
309 }
310 }
311
312 async fn create_signal_step(
314 &self,
315 name: &str,
316 position: u32,
317 input: Value,
318 ) -> Result<Step, EngineError> {
319 let trace_id = step_trace_id(self.run_id, name, position);
320 Ok(self
321 .store
322 .create_step(NewStep {
323 run_id: self.run_id,
324 trace_id,
325 name: name.to_string(),
326 kind: StepKind::Signal,
327 position,
328 input: Some(input),
329 is_error_handler: false,
330 })
331 .await?)
332 }
333}
334
335fn deadline(
337 name: &str,
338 now: DateTime<Utc>,
339 timeout: Duration,
340) -> Result<DateTime<Utc>, EngineError> {
341 let delta = TimeDelta::from_std(timeout).ok();
342 match delta.and_then(|delta| now.checked_add_signed(delta)) {
343 Some(deadline_at) => Ok(deadline_at),
344 None => Err(EngineError::StepConfig(format!(
345 "signal step '{name}' timeout {timeout:?} is out of range"
346 ))),
347 }
348}
349
350fn stored_deadline(name: &str, input: Option<&Value>) -> Result<DateTime<Utc>, EngineError> {
352 let Some(value) = input.and_then(|i| i.get(DEADLINE_AT_KEY)) else {
353 let message = format!("signal step '{name}' has no stored deadline");
354 return Err(EngineError::StepConfig(message));
355 };
356 match from_value(value.clone()) {
357 Ok(deadline_at) => Ok(deadline_at),
358 Err(e) => Err(EngineError::StepConfig(format!(
359 "signal step '{name}' has an invalid deadline: {e}"
360 ))),
361 }
362}
363
364fn require_output(name: &str, output: Option<Value>) -> Result<Value, EngineError> {
366 let Some(output) = output else {
367 let message = format!("signal step '{name}' has no stored output");
368 return Err(EngineError::StepConfig(message));
369 };
370 Ok(output)
371}
372
373fn decode_signal_output<S: Signal>(name: &str, output: &Value) -> Result<Option<S>, EngineError> {
376 if output.get(SIGNAL_TIMED_OUT_KEY).and_then(Value::as_bool) == Some(true) {
377 return Ok(None);
378 }
379 let Some(payload) = output.get("payload") else {
380 let message = format!("signal step '{name}' output has no payload");
381 return Err(EngineError::StepConfig(message));
382 };
383 match from_value::<S>(payload.clone()) {
384 Ok(signal) => Ok(Some(signal)),
385 Err(e) => Err(EngineError::StepConfig(format!(
386 "signal step '{name}' payload does not match the expected type: {e}"
387 ))),
388 }
389}