a3s_flow/engine/
signals.rs1use crate::error::{FlowError, Result};
2use crate::model::{
3 validate_signal_wait, FlowEvent, SignalWaitStatus, WorkflowRunSnapshot, WorkflowRunStatus,
4 WorkflowSignal, WorkflowSignalSnapshot,
5};
6
7use super::{
8 validation::{ensure_signal_wait_command_matches, is_event_conflict},
9 FlowEngine,
10};
11
12pub(super) enum SignalWaitCommandOutcome {
13 Replay,
14 Waiting,
15}
16
17impl FlowEngine {
18 pub async fn send_signal(
26 &self,
27 run_id: &str,
28 signal: WorkflowSignal,
29 ) -> Result<WorkflowRunSnapshot> {
30 let (snapshot, _) = self.send_signal_with_commit(run_id, signal).await?;
31 Ok(snapshot)
32 }
33
34 pub(crate) async fn send_signal_with_commit(
36 &self,
37 run_id: &str,
38 signal: WorkflowSignal,
39 ) -> Result<(WorkflowRunSnapshot, Option<String>)> {
40 signal.validate()?;
41 let mut committed_run_id = None;
42
43 for _ in 0..self.max_replay_iterations {
44 let candidate = self.ensure_continuation_leaf(run_id).await?;
47 if candidate.status == WorkflowRunStatus::Pending {
48 self.ensure_run_started_with_admission(
49 &candidate.run_id,
50 &candidate.spec,
51 &candidate.input,
52 false,
53 )
54 .await?;
55 continue;
56 }
57 let chain = self.continuation_chain(run_id).await?;
58 let leaf = chain
59 .last()
60 .ok_or_else(|| FlowError::RunNotFound(run_id.to_string()))?;
61
62 let existing = chain.iter().find_map(|snapshot| {
63 snapshot
64 .signal(&signal.signal_id)
65 .map(|existing| (snapshot.run_id.as_str(), existing))
66 });
67 if let Some((delivery_run_id, existing)) = existing {
68 ensure_signal_matches(delivery_run_id, existing, &signal)?;
69 match self.recover_and_drive_continuation_leaf(run_id).await {
70 Ok(snapshot) => return Ok((snapshot, committed_run_id)),
71 Err(error) if is_event_conflict(&error) => continue,
72 Err(error) => return Err(error),
73 }
74 }
75
76 if leaf.status.is_terminal() {
77 return Err(FlowError::RunTerminal(leaf.run_id.clone()));
78 }
79 if !leaf.spec.accepts_signal(&signal.name) {
80 return Err(FlowError::InvalidTransition(format!(
81 "workflow run {} does not declare signal {}",
82 leaf.run_id, signal.name
83 )));
84 }
85 self.ensure_runtime_build_available(&leaf.run_id, &leaf.spec)?;
86 match self
87 .record_event_at(
88 &leaf.run_id,
89 leaf.last_sequence,
90 FlowEvent::SignalReceived {
91 signal: signal.clone(),
92 },
93 )
94 .await
95 {
96 Ok(_) => {
97 committed_run_id = Some(leaf.run_id.clone());
98 match self.drive(run_id).await {
99 Ok(snapshot) => return Ok((snapshot, committed_run_id)),
100 Err(error) if is_event_conflict(&error) => continue,
101 Err(error) => return Err(error),
102 }
103 }
104 Err(error) if is_event_conflict(&error) => continue,
105 Err(error) => return Err(error),
106 }
107 }
108
109 Err(FlowError::ReplayLimitExceeded(self.max_replay_iterations))
110 }
111
112 pub(super) async fn reconcile_signal_waits(
116 &self,
117 snapshot: &WorkflowRunSnapshot,
118 ) -> Result<bool> {
119 let mut waits = snapshot
120 .signal_waits
121 .values()
122 .filter(|wait| wait.status == SignalWaitStatus::Waiting)
123 .collect::<Vec<_>>();
124 waits.sort_by_key(|wait| wait.created_sequence);
125
126 for wait in waits {
127 let signal = snapshot
128 .signals
129 .iter()
130 .filter(|signal| signal.consumed_by.is_none() && signal.name == wait.signal_name)
131 .min_by_key(|signal| signal.received_sequence);
132 let Some(signal) = signal else {
133 continue;
134 };
135
136 self.record_event_at(
137 &snapshot.run_id,
138 snapshot.last_sequence,
139 FlowEvent::SignalWaitCompleted {
140 wait_id: wait.wait_id.clone(),
141 signal_id: signal.signal_id.clone(),
142 },
143 )
144 .await?;
145 return Ok(true);
146 }
147
148 Ok(false)
149 }
150
151 pub(super) async fn schedule_signal_wait(
152 &self,
153 snapshot: &WorkflowRunSnapshot,
154 wait_id: String,
155 signal_name: String,
156 ) -> Result<SignalWaitCommandOutcome> {
157 validate_signal_wait(&wait_id, &signal_name)?;
158 if !snapshot.spec.accepts_signal(&signal_name) {
159 return Err(FlowError::InvalidTransition(format!(
160 "workflow run {} does not declare signal {signal_name}",
161 snapshot.run_id
162 )));
163 }
164
165 match snapshot.signal_waits.get(&wait_id) {
166 Some(wait) => {
167 ensure_signal_wait_command_matches(&snapshot.run_id, wait, &signal_name)?;
168 match wait.status {
169 SignalWaitStatus::Completed => Ok(SignalWaitCommandOutcome::Replay),
170 SignalWaitStatus::Waiting => Ok(SignalWaitCommandOutcome::Waiting),
171 SignalWaitStatus::Cancelled => Err(FlowError::InvalidTransition(format!(
172 "workflow rescheduled cancelled signal wait {wait_id}; cancellation cleanup must use a distinct stable identity"
173 ))),
174 }
175 }
176 None => {
177 self.record_event_at(
178 &snapshot.run_id,
179 snapshot.last_sequence,
180 FlowEvent::SignalWaitCreated {
181 wait_id,
182 signal_name,
183 },
184 )
185 .await?;
186 Ok(SignalWaitCommandOutcome::Replay)
187 }
188 }
189 }
190}
191
192fn ensure_signal_matches(
193 run_id: &str,
194 existing: &WorkflowSignalSnapshot,
195 signal: &WorkflowSignal,
196) -> Result<()> {
197 if existing.name != signal.name || existing.payload != signal.payload {
198 return Err(FlowError::SignalConflict {
199 run_id: run_id.to_string(),
200 signal_id: signal.signal_id.clone(),
201 reason: "name or payload differs from the durable delivery".to_string(),
202 });
203 }
204 Ok(())
205}