1use std::sync::Arc;
4
5use aion_core::{Payload, RunId, WorkflowError, WorkflowId};
6use aion_store::EventStore;
7use aion_store::visibility::VisibilityStore;
8use chrono::Utc;
9
10use crate::EngineError;
11use crate::registry::{Registry, TerminalOutcome, WorkflowHandle};
12use crate::runtime::RuntimeHandle;
13
14use super::visibility::upsert_workflow_visibility;
15
16pub struct TerminateWorkflowContext<'a> {
18 pub runtime: &'a RuntimeHandle,
20 pub store: Arc<dyn EventStore>,
22 pub visibility_store: Arc<dyn VisibilityStore>,
24 pub registry: &'a Registry,
26}
27
28pub async fn complete(
36 context: TerminateWorkflowContext<'_>,
37 id: &WorkflowId,
38 run: &RunId,
39 result: Payload,
40) -> Result<(), EngineError> {
41 let handle = registered_handle(context.registry, id, run)?;
42 {
43 let recorder = handle.recorder();
44 let mut recorder = recorder.lock().await;
45 let history = context.store.read_history(id).await?;
46 reject_if_recorded_terminal(&history, id, run)?;
47 recorder
48 .record_workflow_completed(Utc::now(), result.clone())
49 .await?;
50 crate::time::retire_run_deadline(&mut recorder, &history, run).await?;
55 }
56 upsert_workflow_visibility(context.store, context.visibility_store, id, run).await?;
57
58 handle
59 .completion()
60 .notify(TerminalOutcome::Completed(result));
61 context.registry.remove(id, run)?;
62 Ok(())
63}
64
65pub async fn fail(
73 context: TerminateWorkflowContext<'_>,
74 id: &WorkflowId,
75 run: &RunId,
76 error: WorkflowError,
77) -> Result<(), EngineError> {
78 let handle = registered_handle(context.registry, id, run)?;
79 {
80 let recorder = handle.recorder();
81 let mut recorder = recorder.lock().await;
82 let history = context.store.read_history(id).await?;
83 reject_if_recorded_terminal(&history, id, run)?;
84 recorder
85 .record_workflow_failed(Utc::now(), error.clone())
86 .await?;
87 crate::time::retire_run_deadline(&mut recorder, &history, run).await?;
89 }
90 upsert_workflow_visibility(context.store, context.visibility_store, id, run).await?;
91
92 handle.completion().notify(TerminalOutcome::Failed(error));
93 context.registry.remove(id, run)?;
94 Ok(())
95}
96
97pub async fn cancel(
106 context: TerminateWorkflowContext<'_>,
107 id: &WorkflowId,
108 run: &RunId,
109 reason: impl Into<String>,
110) -> Result<(), EngineError> {
111 let handle = registered_handle(context.registry, id, run)?;
112 let reason = reason.into();
113 {
117 let recorder = handle.recorder();
118 let mut recorder = recorder.lock().await;
119 let history = context.store.read_history(id).await?;
120 reject_if_recorded_terminal(&history, id, run)?;
121 recorder
122 .record_workflow_cancelled(Utc::now(), reason.clone())
123 .await?;
124 crate::time::retire_run_deadline(&mut recorder, &history, run).await?;
126 }
127 if let Err(error) = context.runtime.cancel_pid(handle.pid()) {
128 tracing::debug!(
131 workflow_id = %id,
132 run_id = %run,
133 error = %error,
134 "workflow process already exited during cancel"
135 );
136 }
137 upsert_workflow_visibility(context.store, context.visibility_store, id, run).await?;
138
139 handle
140 .completion()
141 .notify(TerminalOutcome::Cancelled(reason));
142 context.registry.remove(id, run)?;
143 Ok(())
144}
145
146fn reject_if_recorded_terminal(
154 history: &[aion_core::Event],
155 id: &WorkflowId,
156 run: &RunId,
157) -> Result<(), EngineError> {
158 if super::completion::terminal_outcome_from_history(history, run).is_some() {
159 return Err(EngineError::Runtime {
160 reason: format!("workflow {id} run {run} already recorded a terminal event"),
161 });
162 }
163 Ok(())
164}
165
166fn registered_handle(
167 registry: &Registry,
168 id: &WorkflowId,
169 run: &RunId,
170) -> Result<WorkflowHandle, EngineError> {
171 registry
172 .get(id, run)?
173 .ok_or_else(|| EngineError::WorkflowNotFound {
174 workflow_type: format!("{id}/{run}"),
175 })
176}
177
178#[cfg(test)]
179mod tests {
180 use std::sync::Arc;
181
182 use aion_core::{Event, Payload, WorkflowStatus};
183 use aion_package::ContentHash;
184 use aion_store::visibility::VisibilityStore;
185 use aion_store::{EventStore, InMemoryStore};
186 use serde_json::json;
187
188 use super::{TerminateWorkflowContext, cancel, complete, fail};
189 use crate::EngineError;
190 use crate::durability::Recorder;
191 use crate::registry::{
192 CompletionNotifier, HandleResidency, Registry, TerminalOutcome, WorkflowHandle,
193 WorkflowHandleParts,
194 };
195 use crate::runtime::{RuntimeConfig, RuntimeHandle};
196
197 struct ActiveWorkflow {
198 store: Arc<dyn EventStore>,
199 visibility_store: Arc<dyn VisibilityStore>,
200 runtime: RuntimeHandle,
201 registry: Registry,
202 handle: WorkflowHandle,
203 }
204
205 fn payload(label: &str) -> Result<Payload, aion_core::PayloadError> {
206 Payload::from_json(&json!({ "label": label }))
207 }
208
209 fn workflow_error(message: &str) -> aion_core::WorkflowError {
210 aion_core::WorkflowError {
211 message: message.to_owned(),
212 details: None,
213 }
214 }
215
216 async fn active_workflow() -> Result<ActiveWorkflow, Box<dyn std::error::Error>> {
217 let backing = Arc::new(InMemoryStore::default());
218 let store: Arc<dyn EventStore> = Arc::clone(&backing) as Arc<dyn EventStore>;
219 let visibility_store: Arc<dyn VisibilityStore> = backing;
220 let runtime = RuntimeHandle::new(RuntimeConfig::new(Some(1)))?;
221 let registry = Registry::default();
222 let workflow_id = aion_core::WorkflowId::new_v4();
223 let run_id = aion_core::RunId::new_v4();
224 let mut recorder = Recorder::new(workflow_id.clone(), Arc::clone(&store));
225 recorder
226 .record_workflow_started(
227 chrono::Utc::now(),
228 crate::durability::WorkflowStartRecord {
229 workflow_type: "checkout".to_owned(),
230 input: payload("input")?,
231 run_id: aion_core::RunId::new(uuid::Uuid::from_u128(1)),
232 parent_run_id: None,
233 package_version: aion_core::PackageVersion::new("a".repeat(64)),
234 },
235 )
236 .await?;
237 let pid = runtime.spawn_test_process_with_trap_exit(true)?;
238 let completion = CompletionNotifier::new();
239 let handle = WorkflowHandle::new(WorkflowHandleParts {
240 workflow_id: workflow_id.clone(),
241 run_id: run_id.clone(),
242 pid,
243 workflow_type: "checkout".to_owned(),
244 namespace: String::from("default"),
245 loaded_version: ContentHash::from_bytes([9; 32]),
246 cached_status: WorkflowStatus::Running,
247 residency: HandleResidency::Resident,
248 recorder,
249 completion,
250 });
251 registry.insert((workflow_id, run_id), handle.clone())?;
252
253 Ok(ActiveWorkflow {
254 store,
255 visibility_store,
256 runtime,
257 registry,
258 handle,
259 })
260 }
261
262 fn context<'a>(
263 runtime: &'a RuntimeHandle,
264 store: Arc<dyn EventStore>,
265 visibility_store: Arc<dyn VisibilityStore>,
266 registry: &'a Registry,
267 ) -> TerminateWorkflowContext<'a> {
268 TerminateWorkflowContext {
269 runtime,
270 store,
271 visibility_store,
272 registry,
273 }
274 }
275
276 #[tokio::test]
277 async fn complete_records_notifies_and_deregisters() -> Result<(), Box<dyn std::error::Error>> {
278 let active = active_workflow().await?;
279 let result = payload("result")?;
280 let mut receiver = active.handle.completion().subscribe();
281
282 complete(
283 context(
284 &active.runtime,
285 active.store.clone(),
286 active.visibility_store.clone(),
287 &active.registry,
288 ),
289 active.handle.workflow_id(),
290 active.handle.run_id(),
291 result.clone(),
292 )
293 .await?;
294 receiver.changed().await?;
295
296 assert_eq!(
297 receiver.borrow().clone(),
298 Some(TerminalOutcome::Completed(result.clone()))
299 );
300 assert_eq!(
301 active
302 .registry
303 .get(active.handle.workflow_id(), active.handle.run_id())?,
304 None
305 );
306 let history = active
307 .store
308 .read_history(active.handle.workflow_id())
309 .await?;
310 match history.as_slice() {
311 [
312 Event::WorkflowStarted { .. },
313 Event::WorkflowCompleted {
314 envelope,
315 result: recorded,
316 },
317 ] => {
318 assert_eq!(envelope.seq, 2);
319 assert_eq!(recorded, &result);
320 }
321 other => return Err(format!("expected started then completed, found {other:?}").into()),
322 }
323 active.runtime.shutdown()?;
324 Ok(())
325 }
326
327 #[tokio::test]
328 async fn fail_records_notifies_and_deregisters() -> Result<(), Box<dyn std::error::Error>> {
329 let active = active_workflow().await?;
330 let error = workflow_error("workflow failed");
331 let mut receiver = active.handle.completion().subscribe();
332
333 fail(
334 context(
335 &active.runtime,
336 active.store.clone(),
337 active.visibility_store.clone(),
338 &active.registry,
339 ),
340 active.handle.workflow_id(),
341 active.handle.run_id(),
342 error.clone(),
343 )
344 .await?;
345 receiver.changed().await?;
346
347 assert_eq!(
348 receiver.borrow().clone(),
349 Some(TerminalOutcome::Failed(error.clone()))
350 );
351 assert_eq!(
352 active
353 .registry
354 .get(active.handle.workflow_id(), active.handle.run_id())?,
355 None
356 );
357 let history = active
358 .store
359 .read_history(active.handle.workflow_id())
360 .await?;
361 match history.as_slice() {
362 [
363 Event::WorkflowStarted { .. },
364 Event::WorkflowFailed {
365 envelope,
366 error: recorded,
367 },
368 ] => {
369 assert_eq!(envelope.seq, 2);
370 assert_eq!(recorded, &error);
371 }
372 other => return Err(format!("expected started then failed, found {other:?}").into()),
373 }
374 active.runtime.shutdown()?;
375 Ok(())
376 }
377
378 #[tokio::test]
379 async fn cancel_kills_linked_children_records_notifies_and_deregisters()
380 -> Result<(), Box<dyn std::error::Error>> {
381 let active = active_workflow().await?;
382 let child = active
383 .runtime
384 .spawn_linked_test_process(active.handle.pid())?;
385 let reason = String::from("caller requested cancellation");
386 let mut receiver = active.handle.completion().subscribe();
387
388 cancel(
389 context(
390 &active.runtime,
391 active.store.clone(),
392 active.visibility_store.clone(),
393 &active.registry,
394 ),
395 active.handle.workflow_id(),
396 active.handle.run_id(),
397 reason.clone(),
398 )
399 .await?;
400 receiver.changed().await?;
401
402 assert!(!active.runtime.is_live(active.handle.pid()));
403 assert!(!active.runtime.is_live(child));
404 assert_eq!(
405 receiver.borrow().clone(),
406 Some(TerminalOutcome::Cancelled(reason.clone()))
407 );
408 assert_eq!(
409 active
410 .registry
411 .get(active.handle.workflow_id(), active.handle.run_id())?,
412 None
413 );
414 let history = active
415 .store
416 .read_history(active.handle.workflow_id())
417 .await?;
418 match history.as_slice() {
419 [
420 Event::WorkflowStarted { .. },
421 Event::WorkflowCancelled {
422 envelope,
423 reason: recorded,
424 },
425 ] => {
426 assert_eq!(envelope.seq, 2);
427 assert_eq!(recorded, &reason);
428 }
429 other => return Err(format!("expected started then cancelled, found {other:?}").into()),
430 }
431 active.runtime.shutdown()?;
432 Ok(())
433 }
434
435 #[tokio::test]
436 async fn complete_cancels_an_armed_deadline() -> Result<(), Box<dyn std::error::Error>> {
437 let active = active_workflow().await?;
441 let run_id = active.handle.run_id().clone();
442 let deadline_id = crate::time::deadline_timer_id(&run_id)?;
443 {
444 let recorder = active.handle.recorder();
445 let mut recorder = recorder.lock().await;
446 recorder
447 .record_timer_started(chrono::Utc::now(), deadline_id.clone(), chrono::Utc::now())
448 .await?;
449 }
450
451 complete(
452 context(
453 &active.runtime,
454 active.store.clone(),
455 active.visibility_store.clone(),
456 &active.registry,
457 ),
458 active.handle.workflow_id(),
459 active.handle.run_id(),
460 payload("result")?,
461 )
462 .await?;
463
464 let history = active
465 .store
466 .read_history(active.handle.workflow_id())
467 .await?;
468 assert_eq!(
469 crate::time::outstanding_deadline_timer(&history, &run_id),
470 None,
471 "complete retires the declared-timeout deadline: {history:#?}"
472 );
473 assert!(
474 history.iter().any(|event| matches!(
475 event,
476 Event::TimerCancelled { timer_id, .. } if timer_id == &deadline_id
477 )),
478 "a TimerCancelled for the deadline is recorded: {history:#?}"
479 );
480 active.runtime.shutdown()?;
481 Ok(())
482 }
483
484 #[tokio::test]
485 async fn cancel_unknown_workflow_returns_not_found() -> Result<(), Box<dyn std::error::Error>> {
486 let runtime = RuntimeHandle::new(RuntimeConfig::new(Some(1)))?;
487 let registry = Registry::default();
488 let workflow_id = aion_core::WorkflowId::new_v4();
489 let run_id = aion_core::RunId::new_v4();
490
491 let result = cancel(
492 context(
493 &runtime,
494 Arc::new(InMemoryStore::default()),
495 Arc::new(InMemoryStore::default()),
496 ®istry,
497 ),
498 &workflow_id,
499 &run_id,
500 "missing workflow",
501 )
502 .await;
503
504 assert!(matches!(result, Err(EngineError::WorkflowNotFound { .. })));
505 runtime.shutdown()?;
506 Ok(())
507 }
508}