aion/engine/api_activity_lease.rs
1//! The activity-lease record [`Engine`] exposes to the server's handoff seam
2//! (WA-010 R2).
3//!
4//! The server's worker-selection path holds no Recorder — invariant 3 gives
5//! each workflow exactly one, owned by the engine — so the lease fact reaches
6//! the run's Recorder the way a signal does ([`Engine::signal`]): a registered
7//! run (resident or suspended) through its registry handle, a paused-and-not-
8//! resident run or an idle workloop through a one-shot recorder resumed at the
9//! recorded head, and a run inside its registration birth window by waiting
10//! the handle out. No path here touches `EventStore::append` directly.
11
12use aion_core::{ActivityId, Event, RunId, WorkerAttribution, WorkflowId, WorkflowStatus};
13use chrono::Utc;
14
15use crate::EngineError;
16use crate::durability::Recorder;
17use crate::registry::WorkflowHandle;
18
19use super::api::{Engine, workflow_not_found};
20use super::delegated::run_has_terminal_history;
21
22impl Engine {
23 /// Record that `worker` accepted `attempt` of `activity_id` on `run`.
24 ///
25 /// The lease is informational: nothing is delivered to the workflow
26 /// process and nothing wakes, because a lease changes no wait the run is
27 /// parked on — it names who is doing the work. It is appended through the
28 /// run's single Recorder, serialising with every timer, signal and
29 /// completion arrival for that run, so concurrent arrivals never race the
30 /// sequence head.
31 ///
32 /// # Errors
33 ///
34 /// Returns [`EngineError::ActivityLeaseAfterTerminal`] when the run has
35 /// already reached a terminal event (nothing recorded),
36 /// [`EngineError::WorkflowNotFound`] when the `(workflow, run)` pair is
37 /// unknown or its handle never appears within the registration birth
38 /// window, and the store or durability error when the append fails.
39 pub async fn record_activity_lease(
40 &self,
41 id: &WorkflowId,
42 run: &RunId,
43 activity_id: ActivityId,
44 attempt: u32,
45 worker: WorkerAttribution,
46 ) -> Result<(), EngineError> {
47 if let Some(handle) = self.registry().get(id, run)? {
48 return record_through_handle(&handle, activity_id, attempt, worker).await;
49 }
50
51 let history = self.store().read_history(id).await?;
52 if run_has_terminal_history(&history, run) {
53 return Err(lease_after_terminal(id, run, activity_id, attempt));
54 }
55
56 // Paused-but-not-resident (crashed while paused, #204) and idle
57 // workloops (registered on the cadence service, no resident process)
58 // have no live handle and are deliberately excluded from respawn, so
59 // waiting the birth window out would only time out. Record through a
60 // one-shot recorder resumed at the recorded head, exactly as a signal
61 // to such a run is recorded.
62 let segment = aion_core::run_segment(&history, run);
63 let paused = matches!(
64 aion_core::status_from_events(segment),
65 WorkflowStatus::Paused
66 );
67 let idle_workloop = match &self.workloop {
68 Some(workloop) => workloop.store.get_workloop(id).await?.is_some(),
69 None => false,
70 };
71 if paused || idle_workloop {
72 let head = history.last().map(Event::seq).unwrap_or_default();
73 let mut recorder = Recorder::resume_at(id.clone(), self.store(), head)
74 .with_visibility(run.clone(), self.visibility_store());
75 recorder
76 .record_activity_leased(Utc::now(), activity_id, attempt, worker)
77 .await?;
78 return Ok(());
79 }
80
81 let handle = self
82 .handle_after_birth_window(id, run, &history)
83 .await?
84 .ok_or_else(|| workflow_not_found(id, run))?;
85 record_through_handle(&handle, activity_id, attempt, worker).await
86 }
87}
88
89/// Append the lease under the handle's recorder lock.
90///
91/// The terminal check and the append are atomic under that lock: the exit
92/// monitor records terminal events through the same recorder, so a lease
93/// racing a completion either lands before the terminal (and is a true fact
94/// about the attempt that then completed) or is refused after it — never
95/// behind it.
96async fn record_through_handle(
97 handle: &WorkflowHandle,
98 activity_id: ActivityId,
99 attempt: u32,
100 worker: WorkerAttribution,
101) -> Result<(), EngineError> {
102 let recorder = handle.recorder();
103 let mut recorder = recorder.lock().await;
104 let history = recorder.read_history().await?;
105 if run_has_terminal_history(&history, handle.run_id()) {
106 return Err(lease_after_terminal(
107 handle.workflow_id(),
108 handle.run_id(),
109 activity_id,
110 attempt,
111 ));
112 }
113 recorder
114 .record_activity_leased(Utc::now(), activity_id, attempt, worker)
115 .await?;
116 Ok(())
117}
118
119fn lease_after_terminal(
120 id: &WorkflowId,
121 run: &RunId,
122 activity_id: ActivityId,
123 attempt: u32,
124) -> EngineError {
125 EngineError::ActivityLeaseAfterTerminal {
126 workflow_id: id.clone(),
127 run_id: run.clone(),
128 activity_id,
129 attempt,
130 }
131}