Skip to main content

aion/registry/
handle.rs

1//! `WorkflowHandle` process id, type, version, status, residency, and completion metadata.
2
3use std::sync::Arc;
4use std::sync::atomic::AtomicU64;
5
6use aion_core::{Payload, RunId, WorkflowError, WorkflowId, WorkflowStatus};
7use aion_package::ContentHash;
8use tokio::sync::{Mutex, watch};
9
10use crate::durability::Recorder;
11
12/// Engine-internal live residency cached on an active workflow handle.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Residency {
15    /// The workflow has a live BEAM process.
16    Resident,
17    /// The workflow is durable but currently has no live process.
18    Suspended,
19}
20
21/// Backward-compatible alias for the engine-internal residency type.
22pub type HandleResidency = Residency;
23
24/// Terminal outcome delivered to result awaiters by later terminal lifecycle transitions.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum TerminalOutcome {
27    /// Workflow completed successfully with a result payload.
28    Completed(Payload),
29    /// Workflow failed terminally.
30    Failed(WorkflowError),
31    /// Workflow was cancelled with the durable cancellation reason.
32    Cancelled(String),
33    /// Workflow exceeded a timeout owned by the timer/signal cluster.
34    TimedOut(String),
35    /// Workflow continued as a new run under the same workflow identifier.
36    ContinuedAsNew {
37        /// Opaque workflow input payload carried into the new run.
38        input: Payload,
39        /// Workflow type override for the new run, when present.
40        workflow_type: Option<String>,
41        /// Run identifier for the current run that continued.
42        parent_run_id: RunId,
43    },
44}
45
46/// Multi-consumer completion notification channel for a workflow execution.
47#[derive(Clone, Debug)]
48pub struct CompletionNotifier {
49    sender: watch::Sender<Option<TerminalOutcome>>,
50}
51
52impl CompletionNotifier {
53    /// Creates an unfulfilled completion notifier.
54    #[must_use]
55    pub fn new() -> Self {
56        let (sender, _receiver) = watch::channel(None);
57        Self { sender }
58    }
59
60    /// Subscribes a result awaiter to the eventual terminal outcome.
61    #[must_use]
62    pub fn subscribe(&self) -> watch::Receiver<Option<TerminalOutcome>> {
63        self.sender.subscribe()
64    }
65
66    /// Publishes the terminal outcome to all current and future subscribers.
67    ///
68    /// The value is stored even when no result waiter is currently subscribed, so
69    /// a waiter that subscribed from a still-held handle before deregistration can
70    /// observe the terminal outcome instead of hanging on an unfulfilled channel.
71    pub fn notify(&self, outcome: TerminalOutcome) {
72        drop(self.sender.send_replace(Some(outcome)));
73    }
74
75    /// Returns true once a terminal outcome has been published.
76    #[must_use]
77    pub fn is_completed(&self) -> bool {
78        self.sender.borrow().is_some()
79    }
80}
81
82impl Default for CompletionNotifier {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl PartialEq for CompletionNotifier {
89    fn eq(&self, other: &Self) -> bool {
90        self.sender.same_channel(&other.sender)
91    }
92}
93
94impl Eq for CompletionNotifier {}
95
96/// Constructor inputs for a live workflow handle.
97pub struct WorkflowHandleParts {
98    /// Logical workflow identifier assigned at start.
99    pub workflow_id: WorkflowId,
100    /// Concrete run identifier assigned at start.
101    pub run_id: RunId,
102    /// Embedded runtime process identifier.
103    pub pid: u64,
104    /// Logical workflow type selected by the caller.
105    pub workflow_type: String,
106    /// Namespace that owns this workflow execution.
107    pub namespace: String,
108    /// Loaded package version selected by the loader.
109    pub loaded_version: ContentHash,
110    /// Cached projection status initialized from the start event.
111    pub cached_status: WorkflowStatus,
112    /// Engine-internal residency initialized for the live process.
113    pub residency: Residency,
114    /// Single-writer recorder created for this workflow history.
115    pub recorder: Recorder,
116    /// Completion notifier created for result awaiters.
117    pub completion: CompletionNotifier,
118}
119
120/// Live workflow process metadata cached in the active-execution registry.
121///
122/// The handle stores only the runtime process identifier value, not a runtime
123/// object or scheduler state. The cached status is reconciled from the durable
124/// event projection by the registry. Residency is engine-internal and separate
125/// from projected workflow status.
126#[derive(Clone)]
127pub struct WorkflowHandle {
128    workflow_id: WorkflowId,
129    run_id: RunId,
130    pid: u64,
131    workflow_type: String,
132    namespace: String,
133    loaded_version: ContentHash,
134    cached_status: WorkflowStatus,
135    residency: Residency,
136    recorder: Arc<Mutex<Recorder>>,
137    completion: CompletionNotifier,
138    deterministic_nif_sequence: Arc<AtomicU64>,
139    activity_ordinal_sequence: Arc<AtomicU64>,
140    timer_ordinal_sequence: Arc<AtomicU64>,
141    child_ordinal_sequence: Arc<AtomicU64>,
142    signal_receive_counts: Arc<dashmap::DashMap<String, u64>>,
143    signal_send_counts: Arc<dashmap::DashMap<String, u64>>,
144}
145
146impl WorkflowHandle {
147    /// Creates a workflow handle from process metadata and start-owned resources.
148    #[must_use]
149    pub fn new(parts: WorkflowHandleParts) -> Self {
150        Self {
151            workflow_id: parts.workflow_id,
152            run_id: parts.run_id,
153            pid: parts.pid,
154            workflow_type: parts.workflow_type,
155            namespace: parts.namespace,
156            loaded_version: parts.loaded_version,
157            cached_status: parts.cached_status,
158            residency: parts.residency,
159            recorder: Arc::new(Mutex::new(parts.recorder)),
160            completion: parts.completion,
161            deterministic_nif_sequence: Arc::new(AtomicU64::new(0)),
162            activity_ordinal_sequence: Arc::new(AtomicU64::new(0)),
163            timer_ordinal_sequence: Arc::new(AtomicU64::new(0)),
164            child_ordinal_sequence: Arc::new(AtomicU64::new(0)),
165            signal_receive_counts: Arc::new(dashmap::DashMap::new()),
166            signal_send_counts: Arc::new(dashmap::DashMap::new()),
167        }
168    }
169
170    /// Allocate `count` consecutive activity correlation ordinals.
171    ///
172    /// The sequence is monotonic per run and shared by every NIF call the
173    /// run makes (handles clone the same counter), so distinct workflow
174    /// steps never collide on correlation keys. A re-spawned run (crash
175    /// recovery, continue-as-new) gets a fresh handle and counter, and its
176    /// replayed code re-allocates the same ordinals deterministically.
177    #[must_use]
178    pub fn allocate_activity_ordinals(&self, count: u64) -> u64 {
179        self.activity_ordinal_sequence
180            .fetch_add(count, std::sync::atomic::Ordering::SeqCst)
181    }
182
183    /// Allocate `count` consecutive child-workflow spawn ordinals.
184    ///
185    /// Same determinism contract as [`Self::allocate_activity_ordinals`]:
186    /// monotonic per run, shared by every NIF call the run makes, and
187    /// re-allocated identically by replayed code on a re-spawned run. The
188    /// n-th allocated ordinal correlates the n-th `spawn_child` call with
189    /// the n-th recorded `ChildWorkflowStarted` in the run's history
190    /// segment, independent of event sequence numbers and of any
191    /// asynchronous-arrival events interleaved between spawns.
192    #[must_use]
193    pub fn allocate_child_ordinals(&self, count: u64) -> u64 {
194        self.child_ordinal_sequence
195            .fetch_add(count, std::sync::atomic::Ordering::SeqCst)
196    }
197
198    /// Allocate `count` consecutive timer ordinals.
199    ///
200    /// Same determinism contract as [`Self::allocate_activity_ordinals`]:
201    /// monotonic per run, shared by every NIF call the run makes, and
202    /// re-allocated identically by replayed code on a re-spawned run. Used
203    /// to derive anonymous timer identities (`sleep`, `with_timeout` scope
204    /// deadlines) that stay stable across crash-recovery replay.
205    #[must_use]
206    pub fn allocate_timer_ordinals(&self, count: u64) -> u64 {
207        self.timer_ordinal_sequence
208            .fetch_add(count, std::sync::atomic::Ordering::SeqCst)
209    }
210
211    /// Activity ordinals allocated so far by this run's execution.
212    ///
213    /// Read-only progress probe: replay re-allocates deterministically, so a
214    /// value below the run segment's recorded `ActivityScheduled` count means
215    /// the run is still mid-replay.
216    #[must_use]
217    pub fn activity_ordinals_allocated(&self) -> u64 {
218        self.activity_ordinal_sequence
219            .load(std::sync::atomic::Ordering::SeqCst)
220    }
221
222    /// Timer ordinals allocated so far by this run's execution.
223    ///
224    /// Same replay-progress contract as [`Self::activity_ordinals_allocated`],
225    /// measured against recorded anonymous `TimerStarted` events.
226    #[must_use]
227    pub fn timer_ordinals_allocated(&self) -> u64 {
228        self.timer_ordinal_sequence
229            .load(std::sync::atomic::Ordering::SeqCst)
230    }
231
232    /// Child-workflow ordinals allocated so far by this run's execution.
233    ///
234    /// Same replay-progress contract as [`Self::activity_ordinals_allocated`],
235    /// measured against recorded `ChildWorkflowStarted` events.
236    #[must_use]
237    pub fn child_ordinals_allocated(&self) -> u64 {
238        self.child_ordinal_sequence
239            .load(std::sync::atomic::Ordering::SeqCst)
240    }
241
242    /// Number of `receive_signal(name)` calls this run has completed.
243    ///
244    /// Drives the run-scoped consumption index for signal awaits: the k-th
245    /// completed receive for a name consumes the k-th recorded
246    /// `SignalReceived` for that name in this run's segment. Replayed code
247    /// re-executes the same receives in order and re-derives the same
248    /// indices; a timed-out receive consumes nothing and does not advance.
249    #[must_use]
250    pub fn signal_receives_consumed(&self, name: &str) -> u64 {
251        self.signal_receive_counts
252            .get(name)
253            .map_or(0, |entry| *entry)
254    }
255
256    /// Advance the completed-receive count for `name` by one.
257    pub fn mark_signal_receive_consumed(&self, name: &str) {
258        *self
259            .signal_receive_counts
260            .entry(name.to_owned())
261            .or_insert(0) += 1;
262    }
263
264    /// Number of `send_signal(name)` calls this run has completed.
265    ///
266    /// Drives the run-scoped correlation index for sends: the k-th completed
267    /// send for a name correlates with the k-th recorded `SignalSent` for
268    /// that name in this run's segment. Replayed code re-executes the same
269    /// sends in order and re-derives the same indices, independent of any
270    /// same-name arrivals recorded around them.
271    #[must_use]
272    pub fn signal_sends_completed(&self, name: &str) -> u64 {
273        self.signal_send_counts.get(name).map_or(0, |entry| *entry)
274    }
275
276    /// Advance the completed-send count for `name` by one.
277    pub fn mark_signal_send_completed(&self, name: &str) {
278        *self.signal_send_counts.entry(name.to_owned()).or_insert(0) += 1;
279    }
280
281    /// Returns the logical workflow identifier.
282    #[must_use]
283    pub const fn workflow_id(&self) -> &WorkflowId {
284        &self.workflow_id
285    }
286
287    /// Returns the concrete run identifier.
288    #[must_use]
289    pub const fn run_id(&self) -> &RunId {
290        &self.run_id
291    }
292
293    /// Returns the embedded runtime process identifier value.
294    #[must_use]
295    pub const fn pid(&self) -> u64 {
296        self.pid
297    }
298
299    /// Returns the logical workflow type / entry module selected by the caller.
300    #[must_use]
301    pub fn workflow_type(&self) -> &str {
302        &self.workflow_type
303    }
304
305    /// Returns the namespace that owns this workflow execution.
306    #[must_use]
307    pub fn namespace(&self) -> &str {
308        &self.namespace
309    }
310
311    /// Returns the loaded workflow package version identifier.
312    #[must_use]
313    pub const fn loaded_version(&self) -> &ContentHash {
314        &self.loaded_version
315    }
316
317    /// Returns the cached workflow status.
318    #[must_use]
319    pub const fn cached_status(&self) -> WorkflowStatus {
320        self.cached_status
321    }
322
323    /// Returns the live residency tracked separately from workflow status.
324    #[must_use]
325    pub const fn residency(&self) -> Residency {
326        self.residency
327    }
328
329    /// Returns the shared single-writer recorder for later lifecycle transitions.
330    #[must_use]
331    pub fn recorder(&self) -> Arc<Mutex<Recorder>> {
332        Arc::clone(&self.recorder)
333    }
334
335    /// Returns the completion notifier created at workflow start.
336    #[must_use]
337    pub const fn completion(&self) -> &CompletionNotifier {
338        &self.completion
339    }
340
341    /// Returns and advances the workflow-local deterministic NIF call sequence.
342    #[must_use]
343    pub fn next_deterministic_nif_sequence(&self) -> u64 {
344        self.deterministic_nif_sequence
345            .fetch_add(1, std::sync::atomic::Ordering::Relaxed)
346    }
347
348    /// Replaces the cached status with the durable event projection result.
349    pub(in crate::registry) const fn replace_projected_status(&mut self, status: WorkflowStatus) {
350        self.cached_status = status;
351    }
352
353    /// Replaces the engine-internal residency without changing projected status.
354    pub(in crate::registry) const fn replace_residency(&mut self, residency: Residency) {
355        self.residency = residency;
356    }
357}
358
359impl std::fmt::Debug for WorkflowHandle {
360    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361        formatter
362            .debug_struct("WorkflowHandle")
363            .field("workflow_id", &self.workflow_id)
364            .field("run_id", &self.run_id)
365            .field("pid", &self.pid)
366            .field("workflow_type", &self.workflow_type)
367            .field("namespace", &self.namespace)
368            .field("loaded_version", &self.loaded_version)
369            .field("cached_status", &self.cached_status)
370            .field("residency", &self.residency)
371            .field("completion", &self.completion)
372            .finish_non_exhaustive()
373    }
374}
375
376impl PartialEq for WorkflowHandle {
377    fn eq(&self, other: &Self) -> bool {
378        self.workflow_id == other.workflow_id
379            && self.run_id == other.run_id
380            && self.pid == other.pid
381            && self.workflow_type == other.workflow_type
382            && self.namespace == other.namespace
383            && self.loaded_version == other.loaded_version
384            && self.cached_status == other.cached_status
385            && self.residency == other.residency
386            && Arc::ptr_eq(&self.recorder, &other.recorder)
387            && self.completion == other.completion
388            && Arc::ptr_eq(
389                &self.deterministic_nif_sequence,
390                &other.deterministic_nif_sequence,
391            )
392    }
393}
394
395impl Eq for WorkflowHandle {}
396
397#[cfg(test)]
398mod tests {
399    use serde_json::json;
400
401    use super::{CompletionNotifier, TerminalOutcome};
402
403    fn payload(label: &str) -> Result<aion_core::Payload, aion_core::PayloadError> {
404        aion_core::Payload::from_json(&json!({ "label": label }))
405    }
406
407    #[test]
408    fn completion_notifier_stores_outcome_without_active_receiver()
409    -> Result<(), aion_core::PayloadError> {
410        let notifier = CompletionNotifier::new();
411        let receiver = notifier.subscribe();
412        drop(receiver);
413        let result = payload("completed")?;
414
415        notifier.notify(TerminalOutcome::Completed(result.clone()));
416        let late_receiver = notifier.subscribe();
417
418        assert_eq!(
419            late_receiver.borrow().clone(),
420            Some(TerminalOutcome::Completed(result))
421        );
422        assert!(notifier.is_completed());
423        Ok(())
424    }
425}