1use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, TimerId, WorkflowId};
6use aion_store::{ReadableEventStore, StoreError};
7use chrono::{DateTime, Utc};
8use dashmap::DashSet;
9
10use crate::engine_seam::{
11 EngineHandle, EngineSeamError, TimerWheelEntry, WorkflowMailboxMessage, WorkflowResidency,
12};
13
14pub struct TimerService {
20 engine: Arc<dyn EngineHandle>,
21 store: Arc<dyn ReadableEventStore>,
22 recorded_at: fn() -> DateTime<Utc>,
23 terminal_updates: DashSet<(WorkflowId, TimerId)>,
24}
25
26struct TerminalUpdateSlot<'a> {
27 terminal_updates: &'a DashSet<(WorkflowId, TimerId)>,
28 key: (WorkflowId, TimerId),
29}
30
31impl Drop for TerminalUpdateSlot<'_> {
32 fn drop(&mut self) {
33 self.terminal_updates.remove(&self.key);
34 }
35}
36
37#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
39pub enum TimerServiceError {
40 #[error("timer store operation failed: {0}")]
42 Store(#[from] StoreError),
43
44 #[error("timer engine operation failed: {0}")]
46 Engine(#[from] EngineSeamError),
47}
48
49impl TimerService {
50 #[must_use]
52 pub fn new(engine: Arc<dyn EngineHandle>, store: Arc<dyn ReadableEventStore>) -> Self {
53 Self::with_recorded_at(engine, store, Utc::now)
54 }
55
56 #[must_use]
58 pub fn with_recorded_at(
59 engine: Arc<dyn EngineHandle>,
60 store: Arc<dyn ReadableEventStore>,
61 recorded_at: fn() -> DateTime<Utc>,
62 ) -> Self {
63 Self {
64 engine,
65 store,
66 recorded_at,
67 terminal_updates: DashSet::new(),
68 }
69 }
70
71 pub async fn schedule(
82 &self,
83 workflow_id: WorkflowId,
84 timer_id: TimerId,
85 fire_at: DateTime<Utc>,
86 ) -> Result<(), TimerServiceError> {
87 self.store
88 .schedule_timer(&workflow_id, &timer_id, fire_at)
89 .await?;
90
91 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
92 self.engine.arm_timer(TimerWheelEntry {
93 process,
94 timer_id,
95 fire_at,
96 })?;
97 }
98
99 Ok(())
100 }
101
102 pub async fn cancel(
119 &self,
120 workflow_id: WorkflowId,
121 timer_id: TimerId,
122 ) -> Result<(), TimerServiceError> {
123 let key = (workflow_id.clone(), timer_id.clone());
124 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
125
126 let result = self.cancel_guarded(workflow_id, timer_id).await;
127 drop(terminal_update_slot);
128 result
129 }
130
131 async fn cancel_guarded(
132 &self,
133 workflow_id: WorkflowId,
134 timer_id: TimerId,
135 ) -> Result<(), TimerServiceError> {
136 if !self.timer_is_live(&workflow_id, &timer_id).await? {
137 return Ok(());
138 }
139
140 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
141 self.engine.disarm_timer(process, &timer_id)?;
142 }
143
144 let event = Event::TimerCancelled {
145 envelope: self.next_envelope(&workflow_id).await?,
146 timer_id,
147 };
148 self.engine.record_workflow_event(&workflow_id, event)?;
149
150 Ok(())
151 }
152
153 pub async fn fire_timer(
163 &self,
164 workflow_id: WorkflowId,
165 timer_id: TimerId,
166 fire_at: DateTime<Utc>,
167 ) -> Result<(), TimerServiceError> {
168 let key = (workflow_id.clone(), timer_id.clone());
169 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
170
171 let result = self
172 .fire_timer_guarded(workflow_id, timer_id, fire_at)
173 .await;
174 drop(terminal_update_slot);
175 result
176 }
177
178 async fn wait_for_terminal_update_slot(
179 &self,
180 key: (WorkflowId, TimerId),
181 ) -> TerminalUpdateSlot<'_> {
182 loop {
183 if self.terminal_updates.insert(key.clone()) {
184 return TerminalUpdateSlot {
185 terminal_updates: &self.terminal_updates,
186 key,
187 };
188 }
189 tokio::task::yield_now().await;
190 }
191 }
192
193 async fn fire_timer_guarded(
194 &self,
195 workflow_id: WorkflowId,
196 timer_id: TimerId,
197 fire_at: DateTime<Utc>,
198 ) -> Result<(), TimerServiceError> {
199 if !self.timer_is_live(&workflow_id, &timer_id).await? {
200 return Ok(());
201 }
202
203 let event = Event::TimerFired {
204 envelope: self.next_envelope(&workflow_id).await?,
205 timer_id: timer_id.clone(),
206 };
207 self.engine.record_workflow_event(&workflow_id, event)?;
208
209 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
210 self.engine.deliver_workflow_message(
211 process,
212 WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
213 )?;
214 }
215
216 Ok(())
217 }
218
219 async fn timer_is_live(
229 &self,
230 workflow_id: &WorkflowId,
231 timer_id: &TimerId,
232 ) -> Result<bool, StoreError> {
233 let history = self.store.read_history(workflow_id).await?;
234 let segment_start = history
235 .iter()
236 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
237 .unwrap_or(0);
238 let segment = &history[segment_start..];
239 let started = segment.iter().any(|event| {
240 matches!(
241 event,
242 Event::TimerStarted { timer_id: recorded, .. } if recorded == timer_id
243 )
244 });
245 let terminal = segment.iter().any(|event| match event {
246 Event::TimerFired {
247 timer_id: recorded, ..
248 }
249 | Event::TimerCancelled {
250 timer_id: recorded, ..
251 } => recorded == timer_id,
252 _ => false,
253 });
254 Ok(started && !terminal)
255 }
256
257 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
258 let history = self.store.read_history(workflow_id).await?;
259 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
260 Ok(EventEnvelope {
261 seq,
262 recorded_at: (self.recorded_at)(),
263 workflow_id: workflow_id.clone(),
264 })
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use std::sync::Arc;
271
272 use aion_core::{Event, EventEnvelope, TimerId, WorkflowId};
273 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
274 use chrono::{DateTime, Utc};
275
276 use super::{TimerService, TimerServiceError};
277 use crate::engine_seam::test_support::{
278 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
279 };
280 use crate::engine_seam::{
281 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
282 };
283
284 fn instant(offset_seconds: i64) -> DateTime<Utc> {
285 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
286 }
287
288 fn workflow_id() -> WorkflowId {
289 WorkflowId::new_v4()
290 }
291
292 fn timer_id() -> TimerId {
293 TimerId::anonymous(7)
294 }
295
296 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
297 let concrete_store = Arc::new(InMemoryStore::default());
298 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
299 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
300 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
301 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
302 (concrete_store, engine, service)
303 }
304
305 fn recorded_at() -> DateTime<Utc> {
306 instant(1)
307 }
308
309 async fn history(
310 store: &InMemoryStore,
311 workflow_id: &WorkflowId,
312 ) -> Result<Vec<Event>, StoreError> {
313 store.read_history(workflow_id).await
314 }
315
316 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
317 events
318 .iter()
319 .filter(|event| {
320 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
321 })
322 .count()
323 }
324
325 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
326 Event::TimerStarted {
327 envelope: EventEnvelope {
328 seq,
329 recorded_at: instant(0),
330 workflow_id: workflow_id.clone(),
331 },
332 timer_id: timer_id.clone(),
333 fire_at: instant(5),
334 }
335 }
336
337 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
338 Event::WorkflowStarted {
339 envelope: EventEnvelope {
340 seq,
341 recorded_at: instant(0),
342 workflow_id: workflow_id.clone(),
343 },
344 workflow_type: "fixture".to_owned(),
345 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
346 run_id: aion_core::RunId::new_v4(),
347 parent_run_id: None,
348 package_version: aion_core::PackageVersion::new("a".repeat(64)),
349 }
350 }
351
352 #[tokio::test]
353 async fn schedule_records_timer_row_without_timer_started_event()
354 -> Result<(), TimerServiceError> {
355 let (store, _engine, service) = service();
356 let workflow_id = workflow_id();
357 let timer_id = timer_id();
358 let fire_at = instant(10);
359
360 service
361 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
362 .await?;
363
364 let expired = store.expired_timers(fire_at).await?;
365 assert_eq!(expired.len(), 1);
366 assert_eq!(expired[0].workflow_id, workflow_id);
367 assert_eq!(expired[0].timer_id, timer_id);
368 assert_eq!(expired[0].fire_at, fire_at);
369
370 assert!(history(&store, &workflow_id).await?.is_empty());
371 Ok(())
372 }
373
374 #[tokio::test]
375 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
376 let process = WorkflowProcessHandle::new(42);
377 let (_store, engine, service) = service();
378 let workflow_id = workflow_id();
379 let timer_id = timer_id();
380 let fire_at = instant(20);
381 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
382
383 service
384 .schedule(workflow_id, timer_id.clone(), fire_at)
385 .await?;
386
387 assert_eq!(
388 engine.armed_timers()?,
389 vec![TimerWheelEntry {
390 process,
391 timer_id,
392 fire_at
393 }]
394 );
395 Ok(())
396 }
397
398 #[tokio::test]
399 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
400 let (store, engine, service) = service();
401 let workflow_id = workflow_id();
402 let timer_id = timer_id();
403 let fire_at = instant(30);
404 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
405
406 service
407 .schedule(workflow_id.clone(), timer_id, fire_at)
408 .await?;
409
410 assert!(engine.armed_timers()?.is_empty());
411 assert!(history(&store, &workflow_id).await?.is_empty());
412 Ok(())
413 }
414
415 #[tokio::test]
416 async fn fire_records_timer_fired_then_delivers_mailbox_message()
417 -> Result<(), TimerServiceError> {
418 let process = WorkflowProcessHandle::new(42);
419 let (store, engine, service) = service();
420 let workflow_id = workflow_id();
421 let timer_id = timer_id();
422 let fire_at = instant(40);
423 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
424 engine.record_workflow_event(
425 &workflow_id,
426 timer_started_event(&workflow_id, &timer_id, 1),
427 )?;
428
429 service
430 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
431 .await?;
432
433 assert_eq!(
434 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
435 1
436 );
437 assert_eq!(
438 engine.delivered_messages()?,
439 vec![(
440 process,
441 DeliveredWorkflowMessage::TimerFired {
442 timer_id: timer_id.clone(),
443 fire_at
444 }
445 )]
446 );
447 assert!(matches!(
448 engine.operations()?.as_slice(),
449 [
450 FakeEngineOperation::EventRecorded {
451 event: Event::TimerStarted { .. },
452 ..
453 },
454 FakeEngineOperation::EventRecorded {
455 workflow_id: recorded_workflow_id,
456 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
457 },
458 FakeEngineOperation::Delivered {
459 process: delivered_process,
460 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
461 }
462 ] if recorded_workflow_id == &workflow_id
463 && recorded_timer_id == &timer_id
464 && delivered_process == &process
465 && delivered_timer_id == &timer_id
466 ));
467 Ok(())
468 }
469
470 #[tokio::test]
471 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
472 -> Result<(), TimerServiceError> {
473 let (store, engine, service) = service();
474 let workflow_id = workflow_id();
475 let timer_id = timer_id();
476 let fire_at = instant(50);
477 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
478 engine.record_workflow_event(
479 &workflow_id,
480 timer_started_event(&workflow_id, &timer_id, 1),
481 )?;
482
483 service
484 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
485 .await?;
486
487 assert_eq!(
488 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
489 1
490 );
491 assert!(engine.delivered_messages()?.is_empty());
492 Ok(())
493 }
494
495 #[tokio::test]
496 async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
497 let process = WorkflowProcessHandle::new(42);
498 let (store, engine, service) = service();
499 let workflow_id = workflow_id();
500 let timer_id = timer_id();
501 let fire_at = instant(60);
502 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
503 engine.record_workflow_event(
504 &workflow_id,
505 timer_started_event(&workflow_id, &timer_id, 1),
506 )?;
507
508 service
509 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
510 .await?;
511 service
512 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
513 .await?;
514
515 assert_eq!(
516 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
517 1
518 );
519 assert_eq!(engine.delivered_messages()?.len(), 1);
520 Ok(())
521 }
522
523 #[tokio::test]
524 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
525 let process = WorkflowProcessHandle::new(42);
526 let (store, engine, service) = service();
527 let workflow_id = workflow_id();
528 let timer_id = timer_id();
529 let fire_at = instant(70);
530 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
531 engine.record_workflow_event(
532 &workflow_id,
533 timer_started_event(&workflow_id, &timer_id, 1),
534 )?;
535 let cancelled = Event::TimerCancelled {
536 envelope: EventEnvelope {
537 seq: 2,
538 recorded_at: instant(69),
539 workflow_id: workflow_id.clone(),
540 },
541 timer_id: timer_id.clone(),
542 };
543 engine.record_workflow_event(&workflow_id, cancelled)?;
544
545 service
546 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
547 .await?;
548
549 let history = history(&store, &workflow_id).await?;
550 assert_eq!(count_timer_fired(&history, &timer_id), 0);
551 assert!(engine.delivered_messages()?.is_empty());
552 Ok(())
553 }
554
555 #[tokio::test]
556 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
557 let process = WorkflowProcessHandle::new(42);
558 let (store, engine, service) = service();
559 let workflow_id = workflow_id();
560 let timer_id = timer_id();
561 let fire_at = instant(80);
562
563 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
564 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
565 engine.record_workflow_event(
566 &workflow_id,
567 timer_started_event(&workflow_id, &timer_id, 1),
568 )?;
569 service
570 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
571 .await?;
572
573 assert_eq!(
574 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
575 1
576 );
577 assert!(engine.delivered_messages()?.is_empty());
578 Ok(())
579 }
580
581 #[tokio::test]
582 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
583 let process = WorkflowProcessHandle::new(42);
584 let (store, engine, service) = service();
585 let workflow_id = workflow_id();
586 let timer_id = timer_id();
587 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
588
589 service
590 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
591 .await?;
592
593 assert!(history(&store, &workflow_id).await?.is_empty());
594 assert!(engine.delivered_messages()?.is_empty());
595 Ok(())
596 }
597
598 #[tokio::test]
599 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
600 {
601 let process = WorkflowProcessHandle::new(42);
602 let (store, engine, service) = service();
603 let workflow_id = workflow_id();
604 let timer_id = timer_id();
605 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
606 engine.record_workflow_event(
608 &workflow_id,
609 timer_started_event(&workflow_id, &timer_id, 1),
610 )?;
611 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
612
613 service
614 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
615 .await?;
616
617 assert_eq!(
618 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
619 0
620 );
621 assert!(engine.delivered_messages()?.is_empty());
622 Ok(())
623 }
624}