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