1use std::time::Duration;
4
5use aion_core::{TimerCancelCause, TimerId, WorkflowId};
6use chrono::{DateTime, Utc};
7
8use crate::time::{TimerService, TimerServiceError};
9
10#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct SleepTimer {
13 pub timer_id: TimerId,
15 pub fire_at: DateTime<Utc>,
17}
18
19#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
21pub enum SleepTimerError {
22 #[error("sleep duration cannot be represented as a chrono duration")]
24 DurationOutOfRange,
25
26 #[error("sleep fire_at timestamp overflowed recorded workflow time")]
28 FireAtOutOfRange,
29
30 #[error("sleep timer scheduling failed: {0}")]
32 Timer(#[from] TimerServiceError),
33}
34
35pub async fn start_timer(
52 service: &TimerService,
53 workflow_id: WorkflowId,
54 timer_id: TimerId,
55 fire_at: DateTime<Utc>,
56 armed_seq: u64,
57) -> Result<(), TimerServiceError> {
58 service
59 .schedule(workflow_id, timer_id, fire_at, armed_seq)
60 .await
61}
62
63pub async fn cancel_timer(
75 service: &TimerService,
76 workflow_id: WorkflowId,
77 timer_id: TimerId,
78) -> Result<(), TimerServiceError> {
79 service
80 .cancel(workflow_id, timer_id, TimerCancelCause::WorkflowIntent)
81 .await
82}
83
84pub async fn sleep(
98 service: &TimerService,
99 workflow_id: WorkflowId,
100 duration: Duration,
101 recorded_now: DateTime<Utc>,
102 sequence_position: u64,
103 armed_seq: u64,
104) -> Result<SleepTimer, SleepTimerError> {
105 let chrono_duration =
106 chrono::Duration::from_std(duration).map_err(|_| SleepTimerError::DurationOutOfRange)?;
107 let fire_at = recorded_now
108 .checked_add_signed(chrono_duration)
109 .ok_or(SleepTimerError::FireAtOutOfRange)?;
110 let timer_id = TimerId::anonymous(sequence_position);
111
112 service
113 .schedule(workflow_id, timer_id.clone(), fire_at, armed_seq)
114 .await?;
115
116 Ok(SleepTimer { timer_id, fire_at })
117}
118
119#[cfg(test)]
120mod tests {
121 use std::sync::Arc;
122 use std::time::Duration;
123
124 use aion_core::{Event, EventEnvelope, IdError, TimerCancelCause, TimerId, WorkflowId};
125 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
126 use chrono::{DateTime, Utc};
127
128 use super::{SleepTimerError, cancel_timer, sleep, start_timer};
129 use crate::engine_seam::test_support::{FakeEngineHandle, FakeEngineOperation};
130 use crate::engine_seam::{
131 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
132 };
133 use crate::time::{TimerService, TimerServiceError};
134
135 #[derive(thiserror::Error, Debug)]
136 enum TestError {
137 #[error(transparent)]
138 Timer(#[from] TimerServiceError),
139 #[error(transparent)]
140 Sleep(#[from] SleepTimerError),
141 #[error(transparent)]
142 Store(#[from] StoreError),
143 #[error(transparent)]
144 Engine(#[from] crate::engine_seam::EngineSeamError),
145 #[error(transparent)]
146 Id(#[from] IdError),
147 }
148
149 fn instant(offset_seconds: i64) -> DateTime<Utc> {
150 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
151 }
152
153 fn recorded_at() -> DateTime<Utc> {
154 instant(1)
155 }
156
157 fn workflow_id() -> WorkflowId {
158 WorkflowId::new_v4()
159 }
160
161 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
162 let concrete_store = Arc::new(InMemoryStore::default());
163 let writable: Arc<dyn WritableEventStore> = concrete_store.clone();
164 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
165 let engine = Arc::new(FakeEngineHandle::recording_to(writable));
166 let service = TimerService::with_recorded_at(engine.clone(), readable, recorded_at);
167 (concrete_store, engine, service)
168 }
169
170 async fn history(
171 store: &InMemoryStore,
172 workflow_id: &WorkflowId,
173 ) -> Result<Vec<Event>, StoreError> {
174 store.read_history(workflow_id).await
175 }
176
177 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
178 Event::TimerStarted {
179 envelope: EventEnvelope {
180 seq,
181 recorded_at: instant(0),
182 workflow_id: workflow_id.clone(),
183 },
184 timer_id: timer_id.clone(),
185 fire_at: instant(5),
186 }
187 }
188
189 fn count_cancelled(events: &[Event], timer_id: &TimerId) -> usize {
190 events
191 .iter()
192 .filter(|event| {
193 matches!(event, Event::TimerCancelled { timer_id: recorded, .. } if recorded == timer_id)
194 })
195 .count()
196 }
197
198 fn count_fired(events: &[Event], timer_id: &TimerId) -> usize {
199 events
200 .iter()
201 .filter(|event| {
202 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
203 })
204 .count()
205 }
206
207 #[tokio::test]
208 async fn start_timer_preserves_named_id_in_history_and_timer_row() -> Result<(), TestError> {
209 let (store, _engine, service) = service();
210 let workflow_id = workflow_id();
211 let timer_id = TimerId::named("deadline")?;
212 let fire_at = instant(10);
213
214 start_timer(&service, workflow_id.clone(), timer_id.clone(), fire_at, 1).await?;
215
216 let expired = store.expired_timers(fire_at).await?;
217 assert_eq!(expired.len(), 1);
218 assert_eq!(expired[0].workflow_id, workflow_id);
219 assert_eq!(expired[0].timer_id, timer_id);
220 assert_eq!(expired[0].fire_at, fire_at);
221
222 let history = history(&store, &workflow_id).await?;
224 assert!(history.is_empty());
225 Ok(())
226 }
227
228 #[tokio::test]
229 async fn cancel_timer_disarms_resident_wheel_and_records_cancelled() -> Result<(), TestError> {
230 let process = WorkflowProcessHandle::new(42);
231 let (store, engine, service) = service();
232 let workflow_id = workflow_id();
233 let timer_id = TimerId::named("deadline")?;
234 let fire_at = instant(20);
235 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
236 engine.record_workflow_event(
237 &workflow_id,
238 timer_started_event(&workflow_id, &timer_id, 1),
239 )?;
240
241 start_timer(&service, workflow_id.clone(), timer_id.clone(), fire_at, 1).await?;
242 cancel_timer(&service, workflow_id.clone(), timer_id.clone()).await?;
243
244 assert!(engine.armed_timers()?.is_empty());
245 let history = history(&store, &workflow_id).await?;
246 assert_eq!(count_cancelled(&history, &timer_id), 1);
247 assert!(matches!(
250 history.as_slice(),
251 [
252 Event::TimerStarted { .. },
253 Event::TimerCancelled {
254 envelope,
255 timer_id: recorded,
256 cause: TimerCancelCause::WorkflowIntent,
257 }
258 ] if envelope.seq == 2 && recorded == &timer_id
259 ));
260 assert!(engine.operations()?.iter().any(|operation| matches!(
261 operation,
262 FakeEngineOperation::TimerDisarmed { process: disarmed_process, timer_id: disarmed }
263 if disarmed_process == &process && disarmed == &timer_id
264 )));
265 Ok(())
266 }
267
268 #[tokio::test]
269 async fn cancel_timer_after_fire_is_noop() -> Result<(), TestError> {
270 let process = WorkflowProcessHandle::new(42);
271 let (store, engine, service) = service();
272 let workflow_id = workflow_id();
273 let timer_id = TimerId::named("deadline")?;
274 let fire_at = instant(30);
275 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
276 engine.record_workflow_event(
277 &workflow_id,
278 timer_started_event(&workflow_id, &timer_id, 1),
279 )?;
280
281 start_timer(&service, workflow_id.clone(), timer_id.clone(), fire_at, 1).await?;
282 service
283 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
284 .await?;
285 let operation_count = engine.operations()?.len();
286
287 cancel_timer(&service, workflow_id.clone(), timer_id.clone()).await?;
288
289 let history = history(&store, &workflow_id).await?;
290 assert_eq!(count_fired(&history, &timer_id), 1);
291 assert_eq!(count_cancelled(&history, &timer_id), 0);
292 assert_eq!(engine.operations()?.len(), operation_count);
293 Ok(())
294 }
295
296 #[tokio::test]
297 async fn cancel_timer_after_cancel_is_idempotent_noop() -> Result<(), TestError> {
298 let process = WorkflowProcessHandle::new(42);
299 let (store, engine, service) = service();
300 let workflow_id = workflow_id();
301 let timer_id = TimerId::named("deadline")?;
302 let fire_at = instant(40);
303 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
304 engine.record_workflow_event(
305 &workflow_id,
306 timer_started_event(&workflow_id, &timer_id, 1),
307 )?;
308
309 start_timer(&service, workflow_id.clone(), timer_id.clone(), fire_at, 1).await?;
310 cancel_timer(&service, workflow_id.clone(), timer_id.clone()).await?;
311 let operation_count = engine.operations()?.len();
312
313 cancel_timer(&service, workflow_id.clone(), timer_id.clone()).await?;
314
315 let history = history(&store, &workflow_id).await?;
316 assert_eq!(count_cancelled(&history, &timer_id), 1);
317 assert_eq!(engine.operations()?.len(), operation_count);
318 Ok(())
319 }
320
321 #[tokio::test]
322 async fn cancel_timer_settles_anonymous_scope_deadline() -> Result<(), TestError> {
323 let process = WorkflowProcessHandle::new(42);
328 let (store, engine, service) = service();
329 let workflow_id = workflow_id();
330 let timer_id = TimerId::anonymous(42);
331 let fire_at = instant(40);
332 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
333 engine.record_workflow_event(
334 &workflow_id,
335 timer_started_event(&workflow_id, &timer_id, 1),
336 )?;
337 service
338 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
339 .await?;
340
341 cancel_timer(&service, workflow_id.clone(), timer_id.clone()).await?;
342
343 let history = history(&store, &workflow_id).await?;
344 assert_eq!(count_cancelled(&history, &timer_id), 1);
345 assert_eq!(count_fired(&history, &timer_id), 0);
346 Ok(())
347 }
348
349 #[tokio::test]
350 async fn sleep_derives_anonymous_id_and_fire_at_from_recorded_inputs() -> Result<(), TestError>
351 {
352 let (store, _engine, service) = service();
353 let workflow_id = workflow_id();
354 let recorded_now = instant(50);
355 let duration = Duration::from_secs(15);
356 let sequence_position = 9;
357 let expected_timer_id = TimerId::anonymous(sequence_position);
358 let expected_fire_at = instant(65);
359
360 let scheduled = sleep(
361 &service,
362 workflow_id.clone(),
363 duration,
364 recorded_now,
365 sequence_position,
366 1,
367 )
368 .await?;
369
370 assert_eq!(scheduled.timer_id, expected_timer_id);
371 assert_eq!(scheduled.fire_at, expected_fire_at);
372 let history = history(&store, &workflow_id).await?;
374 assert!(history.is_empty());
375 Ok(())
376 }
377
378 #[tokio::test]
379 async fn start_timer_arms_named_timer_without_rewriting_id() -> Result<(), TestError> {
380 let process = WorkflowProcessHandle::new(42);
381 let (_store, engine, service) = service();
382 let workflow_id = workflow_id();
383 let timer_id = TimerId::named("deadline")?;
384 let fire_at = instant(70);
385 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
386
387 start_timer(&service, workflow_id, timer_id.clone(), fire_at, 1).await?;
388
389 assert_eq!(
390 engine.armed_timers()?,
391 vec![TimerWheelEntry {
392 process,
393 timer_id,
394 fire_at,
395 }]
396 );
397 Ok(())
398 }
399}