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 if recorder.record(name, StepKind::Signal, &self.workflow_name, None) {
120 recorder.set_last(vec![name.to_string()]);
121 }
122 }
123 return Ok(from_value::<S>(json!({})).ok());
124 }
125
126 let position = self.next_position();
127
128 if let Some(existing) = self.replay_steps.get(&position).cloned() {
130 check_replay_identity(&existing, position, name, &StepKind::Signal)?;
131 self.last_step_ids = vec![existing.id];
132 return self.signal_replay::<S>(name, key, existing).await;
133 }
134
135 let schema = to_value(schema_for!(S))?;
136 let now = Utc::now();
137 let deadline_at = deadline(name, now, timeout)?;
138 let input = json!({
139 "name": S::NAME,
140 "key": key,
141 SIGNAL_SCHEMA_KEY: schema,
142 "waiting_since": now,
143 DEADLINE_AT_KEY: deadline_at,
144 });
145
146 if let Some(interceptor) = self.interceptor.clone()
148 && let Some(outcome) = interceptor.intercept_signal(name, S::NAME, key, &schema)
149 {
150 let step = self.create_signal_step(name, position, input).await?;
151 self.start_step(step.id, now).await?;
152 self.last_step_ids = vec![step.id];
153
154 let output = match outcome {
155 SignalOutcome::Received(payload) => json!({
156 SIGNAL_TIMED_OUT_KEY: false,
157 "signal_id": null,
158 "payload": payload,
159 }),
160 SignalOutcome::TimedOut => timed_out_output(),
161 };
162 self.store
163 .update_step(
164 step.id,
165 StepUpdate {
166 status: Some(StepStatus::Completed),
167 output: Some(output.clone()),
168 completed_at: Some(Utc::now()),
169 ..StepUpdate::default()
170 },
171 )
172 .await?;
173 info!(
174 run_id = %self.run_id,
175 step = %name,
176 position,
177 "signal step resolved by the step interceptor"
178 );
179 return decode_signal_output(name, &output);
180 }
181
182 let step = self.create_signal_step(name, position, input).await?;
185 self.start_step(step.id, now).await?;
186 self.last_step_ids = vec![step.id];
187
188 if let Some(output) = self.received_signal::<S>(step.id, key).await? {
189 info!(
190 run_id = %self.run_id,
191 step = %name,
192 position,
193 "signal already received, not suspending"
194 );
195 return decode_signal_output(name, &output);
196 }
197
198 info!(
199 run_id = %self.run_id,
200 step = %name,
201 signal = %S::NAME,
202 key = %key,
203 deadline_at = %deadline_at,
204 "waiting for signal"
205 );
206 Err(EngineError::SignalWaiting {
207 run_id: self.run_id,
208 step_id: step.id,
209 step_name: name.to_string(),
210 name: S::NAME.to_string(),
211 key: key.to_string(),
212 deadline_at,
213 })
214 }
215
216 async fn signal_replay<S: Signal>(
218 &mut self,
219 name: &str,
220 key: &str,
221 existing: Step,
222 ) -> Result<Option<S>, EngineError> {
223 match existing.status.state {
224 StepStatus::Completed => {
225 let output = require_output(name, existing.output)?;
226 info!(
227 run_id = %self.run_id,
228 step = %name,
229 "signal step replayed (resolved)"
230 );
231 decode_signal_output(name, &output)
232 }
233 StepStatus::Running => {
234 let deadline_at = stored_deadline(name, existing.input.as_ref())?;
235
236 if Utc::now() >= deadline_at {
237 let resolution = self
238 .store
239 .resolve_signal_step(existing.id, timed_out_output())
240 .await?;
241 let output = match resolution {
242 SignalStepResolution::Resolved { .. } => timed_out_output(),
243 SignalStepResolution::NotWaiting { output } => {
245 require_output(name, output)?
246 }
247 };
248 info!(
249 run_id = %self.run_id,
250 step = %name,
251 "signal step deadline reached"
252 );
253 return decode_signal_output(name, &output);
254 }
255
256 if let Some(output) = self.received_signal::<S>(existing.id, key).await? {
259 return decode_signal_output(name, &output);
260 }
261 info!(
262 run_id = %self.run_id,
263 step = %name,
264 deadline_at = %deadline_at,
265 "signal still not received, suspending again"
266 );
267 Err(EngineError::SignalWaiting {
268 run_id: self.run_id,
269 step_id: existing.id,
270 step_name: name.to_string(),
271 name: S::NAME.to_string(),
272 key: key.to_string(),
273 deadline_at,
274 })
275 }
276 state => Err(EngineError::StepConfig(format!(
277 "signal step '{name}' is in state {state:?}"
278 ))),
279 }
280 }
281
282 async fn received_signal<S: Signal>(
288 &self,
289 step_id: Uuid,
290 key: &str,
291 ) -> Result<Option<Value>, EngineError> {
292 let since = self.run_created_at().await?;
293 let signals = self.store.list_signals_for_key(S::NAME, key, since).await?;
294 let Some(signal) = signals
295 .iter()
296 .find(|s| from_value::<S>(s.payload.clone()).is_ok())
297 else {
298 return Ok(None);
299 };
300
301 let output = received_output(signal);
302 let resolution = self
303 .store
304 .resolve_signal_step(step_id, output.clone())
305 .await?;
306 match resolution {
307 SignalStepResolution::Resolved { .. } => Ok(Some(output)),
308 SignalStepResolution::NotWaiting { output } => Ok(output),
310 }
311 }
312
313 async fn create_signal_step(
315 &self,
316 name: &str,
317 position: u32,
318 input: Value,
319 ) -> Result<Step, EngineError> {
320 let trace_id = step_trace_id(self.run_id, name, position);
321 Ok(self
322 .store
323 .create_step(NewStep {
324 run_id: self.run_id,
325 trace_id,
326 name: name.to_string(),
327 kind: StepKind::Signal,
328 position,
329 input: Some(input),
330 is_error_handler: false,
331 })
332 .await?)
333 }
334}
335
336fn deadline(
338 name: &str,
339 now: DateTime<Utc>,
340 timeout: Duration,
341) -> Result<DateTime<Utc>, EngineError> {
342 let delta = TimeDelta::from_std(timeout).ok();
343 match delta.and_then(|delta| now.checked_add_signed(delta)) {
344 Some(deadline_at) => Ok(deadline_at),
345 None => Err(EngineError::StepConfig(format!(
346 "signal step '{name}' timeout {timeout:?} is out of range"
347 ))),
348 }
349}
350
351fn stored_deadline(name: &str, input: Option<&Value>) -> Result<DateTime<Utc>, EngineError> {
353 let Some(value) = input.and_then(|i| i.get(DEADLINE_AT_KEY)) else {
354 let message = format!("signal step '{name}' has no stored deadline");
355 return Err(EngineError::StepConfig(message));
356 };
357 match from_value(value.clone()) {
358 Ok(deadline_at) => Ok(deadline_at),
359 Err(e) => Err(EngineError::StepConfig(format!(
360 "signal step '{name}' has an invalid deadline: {e}"
361 ))),
362 }
363}
364
365fn require_output(name: &str, output: Option<Value>) -> Result<Value, EngineError> {
367 let Some(output) = output else {
368 let message = format!("signal step '{name}' has no stored output");
369 return Err(EngineError::StepConfig(message));
370 };
371 Ok(output)
372}
373
374fn decode_signal_output<S: Signal>(name: &str, output: &Value) -> Result<Option<S>, EngineError> {
377 if output.get(SIGNAL_TIMED_OUT_KEY).and_then(Value::as_bool) == Some(true) {
378 return Ok(None);
379 }
380 let Some(payload) = output.get("payload") else {
381 let message = format!("signal step '{name}' output has no payload");
382 return Err(EngineError::StepConfig(message));
383 };
384 match from_value::<S>(payload.clone()) {
385 Ok(signal) => Ok(Some(signal)),
386 Err(e) => Err(EngineError::StepConfig(format!(
387 "signal step '{name}' payload does not match the expected type: {e}"
388 ))),
389 }
390}