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