1use std::collections::HashMap;
4
5use aion_core::{Event, TimerId};
6
7#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9pub enum CorrelationKey {
10 Activity(
12 u64,
14 ),
15 Child(
23 u64,
25 ),
26 Timer(
28 TimerId,
30 ),
31 Signal {
33 name: String,
35 index: usize,
37 },
38}
39
40#[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#[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 u64::try_from(prior_starts).ok().map(CorrelationKey::Child)
89 }
90 _ => key_for_positionless_event(event),
91 }
92}
93
94#[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 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}