Skip to main content

aion/durability/
correlation.rs

1//! Correlation keys + matching rules.
2
3use std::collections::HashMap;
4
5use aion_core::{Event, TimerId};
6
7/// Deterministic identity for one world-touching workflow call.
8#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub enum CorrelationKey {
10    /// Activity scheduled at the deterministic ordinal carried by its [`aion_core::ActivityId`].
11    Activity(
12        /// Deterministic scheduling ordinal.
13        u64,
14    ),
15    /// Child workflow scheduled at the deterministic spawn ordinal.
16    ///
17    /// The ordinal is positional: the n-th `spawn_child` call a run makes
18    /// correlates with the n-th recorded `ChildWorkflowStarted` in that run's
19    /// history segment. Like activity ordinals it restarts at zero for every
20    /// run, so replay re-derives the same identity regardless of how many
21    /// asynchronous-arrival events (signals, timer fires) interleave.
22    Child(
23        /// Zero-based spawn ordinal within the run segment.
24        u64,
25    ),
26    /// Timer selected by workflow code or assigned by the engine.
27    Timer(
28        /// Timer identifier from the recorded timer-start event.
29        TimerId,
30    ),
31    /// Signal delivery by name and zero-based occurrence for that name in history order.
32    Signal {
33        /// Signal name selected by workflow code.
34        name: String,
35        /// Zero-based occurrence of this signal name in recorded history order.
36        index: usize,
37    },
38}
39
40/// Derives correlation keys for every event in an ordered history.
41///
42/// Non-world-touching events and activity/timer/child outcome events do not introduce a new call
43/// key, so their slots contain `None`. Signal keys carry the per-name occurrence index and child
44/// workflow start keys carry the positional spawn ordinal, both derived from event order within
45/// the supplied history slice.
46#[must_use]
47pub fn correlation_keys_for_history(events: &[Event]) -> Vec<Option<CorrelationKey>> {
48    let mut counters = OccurrenceCounters::default();
49    events
50        .iter()
51        .map(|event| key_for_event_with_counters(event, &mut counters))
52        .collect()
53}
54
55/// Derives the correlation key for one event in an ordered history.
56///
57/// `index` is the event's index inside `events`; it is used to count prior signals with the same
58/// name and prior child workflow starts, so signal occurrence indices and child spawn ordinals are
59/// positional within the slice.
60#[must_use]
61pub fn key_for_event(events: &[Event], index: usize) -> Option<CorrelationKey> {
62    let event = events.get(index)?;
63    match event {
64        Event::SignalReceived { name, .. } | Event::SignalSent { name, .. } => {
65            let prior_same_name = events
66                .iter()
67                .take(index)
68                .filter(|prior| matches!(prior, Event::SignalReceived { name: prior_name, .. } | Event::SignalSent { name: prior_name, .. } if prior_name == name))
69                .count();
70            Some(CorrelationKey::Signal {
71                name: name.clone(),
72                index: prior_same_name,
73            })
74        }
75        Event::ChildWorkflowStarted { .. } => {
76            let prior_starts = events
77                .iter()
78                .take(index)
79                .filter(|prior| matches!(prior, Event::ChildWorkflowStarted { .. }))
80                .count();
81            // On the 64-bit targets this engine ships on, usize -> u64 never
82            // fails; the fallible conversion exists only to satisfy the type
83            // signature on hypothetical wider-usize platforms. There, an
84            // overflowing count would leave this start event keyless: strict
85            // replay reports a mismatch when resolution reaches it, but the
86            // live fast-forward path skips keyless events silently — a
87            // missing key is not guaranteed to surface as an error.
88            u64::try_from(prior_starts).ok().map(CorrelationKey::Child)
89        }
90        _ => key_for_positionless_event(event),
91    }
92}
93
94/// Running occurrence counts used to derive positional keys in one history pass.
95#[derive(Default)]
96struct OccurrenceCounters {
97    signal_counts: HashMap<String, usize>,
98    child_spawns: u64,
99}
100
101fn key_for_event_with_counters(
102    event: &Event,
103    counters: &mut OccurrenceCounters,
104) -> Option<CorrelationKey> {
105    match event {
106        Event::SignalReceived { name, .. } | Event::SignalSent { name, .. } => {
107            let index = counters
108                .signal_counts
109                .get(name)
110                .copied()
111                .unwrap_or_default();
112            counters.signal_counts.insert(name.clone(), index + 1);
113            Some(CorrelationKey::Signal {
114                name: name.clone(),
115                index,
116            })
117        }
118        Event::ChildWorkflowStarted { .. } => {
119            let ordinal = counters.child_spawns;
120            counters.child_spawns += 1;
121            Some(CorrelationKey::Child(ordinal))
122        }
123        _ => key_for_positionless_event(event),
124    }
125}
126
127fn key_for_positionless_event(event: &Event) -> Option<CorrelationKey> {
128    match event {
129        Event::ActivityScheduled { activity_id, .. } => {
130            Some(CorrelationKey::Activity(activity_id.sequence_position()))
131        }
132        Event::TimerStarted { timer_id, .. } => Some(CorrelationKey::Timer(timer_id.clone())),
133        _ => None,
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use aion_core::{Event, EventEnvelope, Payload, WorkflowId};
140    use chrono::Utc;
141    use serde_json::json;
142    use uuid::Uuid;
143
144    use super::{CorrelationKey, correlation_keys_for_history, key_for_event};
145
146    fn envelope(seq: u64) -> EventEnvelope {
147        EventEnvelope {
148            seq,
149            recorded_at: Utc::now(),
150            workflow_id: WorkflowId::new(Uuid::nil()),
151        }
152    }
153
154    fn payload() -> Result<Payload, Box<dyn std::error::Error>> {
155        Ok(Payload::from_json(&json!(null))?)
156    }
157
158    #[test]
159    fn derives_signal_occurrence_indices_by_name() -> Result<(), Box<dyn std::error::Error>> {
160        let history = vec![
161            Event::SignalReceived {
162                envelope: envelope(1),
163                name: "ready".to_owned(),
164                payload: payload()?,
165            },
166            Event::SignalReceived {
167                envelope: envelope(2),
168                name: "other".to_owned(),
169                payload: payload()?,
170            },
171            Event::SignalReceived {
172                envelope: envelope(3),
173                name: "ready".to_owned(),
174                payload: payload()?,
175            },
176        ];
177
178        let keys = correlation_keys_for_history(&history);
179
180        assert_eq!(
181            keys,
182            vec![
183                Some(CorrelationKey::Signal {
184                    name: "ready".to_owned(),
185                    index: 0,
186                }),
187                Some(CorrelationKey::Signal {
188                    name: "other".to_owned(),
189                    index: 0,
190                }),
191                Some(CorrelationKey::Signal {
192                    name: "ready".to_owned(),
193                    index: 1,
194                }),
195            ]
196        );
197        Ok(())
198    }
199
200    fn child_started(seq: u64, child: u128) -> Result<Event, Box<dyn std::error::Error>> {
201        Ok(Event::ChildWorkflowStarted {
202            envelope: envelope(seq),
203            child_workflow_id: WorkflowId::new(Uuid::from_u128(child)),
204            workflow_type: "child".to_owned(),
205            input: payload()?,
206            package_version: aion_core::PackageVersion::new("a".repeat(64)),
207        })
208    }
209
210    #[test]
211    fn derives_positional_child_ordinals_independent_of_sequence_numbers()
212    -> Result<(), Box<dyn std::error::Error>> {
213        // Deliberately sparse, late sequence numbers: positional ordinals
214        // must not be derived from event sequence values.
215        let history = vec![child_started(41, 1)?, child_started(97, 2)?];
216
217        let keys = correlation_keys_for_history(&history);
218
219        assert_eq!(
220            keys,
221            vec![
222                Some(CorrelationKey::Child(0)),
223                Some(CorrelationKey::Child(1)),
224            ]
225        );
226        assert_eq!(key_for_event(&history, 0), Some(CorrelationKey::Child(0)));
227        assert_eq!(key_for_event(&history, 1), Some(CorrelationKey::Child(1)));
228        Ok(())
229    }
230
231    #[test]
232    fn interleaved_async_arrivals_do_not_shift_child_ordinals()
233    -> Result<(), Box<dyn std::error::Error>> {
234        let history = vec![
235            child_started(1, 1)?,
236            Event::SignalReceived {
237                envelope: envelope(2),
238                name: "mid".to_owned(),
239                payload: payload()?,
240            },
241            Event::ChildWorkflowCompleted {
242                envelope: envelope(3),
243                child_workflow_id: WorkflowId::new(Uuid::from_u128(1)),
244                result: payload()?,
245            },
246            child_started(4, 2)?,
247        ];
248
249        let keys = correlation_keys_for_history(&history);
250
251        assert_eq!(
252            keys,
253            vec![
254                Some(CorrelationKey::Child(0)),
255                Some(CorrelationKey::Signal {
256                    name: "mid".to_owned(),
257                    index: 0,
258                }),
259                None,
260                Some(CorrelationKey::Child(1)),
261            ]
262        );
263        assert_eq!(key_for_event(&history, 3), Some(CorrelationKey::Child(1)));
264        Ok(())
265    }
266}