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(
234 &self,
235 workflow_id: &WorkflowId,
236 timer_id: &TimerId,
237 ) -> Result<bool, StoreError> {
238 let history = self.store.read_history(workflow_id).await?;
239 Ok(live_timers_in_active_segment(&history).contains(timer_id))
240 }
241
242 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
243 let history = self.store.read_history(workflow_id).await?;
244 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
245 Ok(EventEnvelope {
246 seq,
247 recorded_at: (self.recorded_at)(),
248 workflow_id: workflow_id.clone(),
249 })
250 }
251}
252
253pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
268 let segment_start = history
269 .iter()
270 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
271 .unwrap_or(0);
272 let mut live: Vec<TimerId> = Vec::new();
273 for event in &history[segment_start..] {
274 match event {
275 Event::TimerStarted { timer_id, .. } => {
276 if !live.contains(timer_id) {
277 live.push(timer_id.clone());
278 }
279 }
280 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
281 live.retain(|id| id != timer_id);
282 }
283 _ => {}
284 }
285 }
286 live
287}
288
289#[cfg(test)]
290mod tests {
291 use std::sync::Arc;
292
293 use aion_core::{Event, EventEnvelope, TimerId, WorkflowId};
294 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
295 use chrono::{DateTime, Utc};
296
297 use super::{TimerService, TimerServiceError, live_timers_in_active_segment};
298 use crate::engine_seam::test_support::{
299 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
300 };
301 use crate::engine_seam::{
302 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
303 };
304
305 fn instant(offset_seconds: i64) -> DateTime<Utc> {
306 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
307 }
308
309 fn workflow_id() -> WorkflowId {
310 WorkflowId::new_v4()
311 }
312
313 fn timer_id() -> TimerId {
314 TimerId::anonymous(7)
315 }
316
317 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
318 let concrete_store = Arc::new(InMemoryStore::default());
319 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
320 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
321 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
322 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
323 (concrete_store, engine, service)
324 }
325
326 fn recorded_at() -> DateTime<Utc> {
327 instant(1)
328 }
329
330 async fn history(
331 store: &InMemoryStore,
332 workflow_id: &WorkflowId,
333 ) -> Result<Vec<Event>, StoreError> {
334 store.read_history(workflow_id).await
335 }
336
337 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
338 events
339 .iter()
340 .filter(|event| {
341 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
342 })
343 .count()
344 }
345
346 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
347 Event::TimerStarted {
348 envelope: EventEnvelope {
349 seq,
350 recorded_at: instant(0),
351 workflow_id: workflow_id.clone(),
352 },
353 timer_id: timer_id.clone(),
354 fire_at: instant(5),
355 }
356 }
357
358 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
359 Event::WorkflowStarted {
360 envelope: EventEnvelope {
361 seq,
362 recorded_at: instant(0),
363 workflow_id: workflow_id.clone(),
364 },
365 workflow_type: "fixture".to_owned(),
366 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
367 run_id: aion_core::RunId::new_v4(),
368 parent_run_id: None,
369 package_version: aion_core::PackageVersion::new("a".repeat(64)),
370 }
371 }
372
373 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
374 Event::TimerFired {
375 envelope: EventEnvelope {
376 seq,
377 recorded_at: instant(0),
378 workflow_id: workflow_id.clone(),
379 },
380 timer_id: timer_id.clone(),
381 }
382 }
383
384 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
385 Event::TimerCancelled {
386 envelope: EventEnvelope {
387 seq,
388 recorded_at: instant(0),
389 workflow_id: workflow_id.clone(),
390 },
391 timer_id: timer_id.clone(),
392 }
393 }
394
395 fn make_named(name: &str) -> TimerId {
396 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
400 }
401
402 fn named_timer_id() -> TimerId {
403 make_named("review-deadline")
404 }
405
406 #[test]
409 fn started_timer_is_live() {
410 let workflow_id = workflow_id();
411 let timer_id = named_timer_id();
412 let history = vec![
413 workflow_started_event(&workflow_id, 0),
414 timer_started_event(&workflow_id, &timer_id, 1),
415 ];
416 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
417 }
418
419 #[test]
420 fn started_then_fired_timer_is_dead() {
421 let workflow_id = workflow_id();
422 let timer_id = named_timer_id();
423 let history = vec![
424 workflow_started_event(&workflow_id, 0),
425 timer_started_event(&workflow_id, &timer_id, 1),
426 timer_fired_event(&workflow_id, &timer_id, 2),
427 ];
428 assert!(live_timers_in_active_segment(&history).is_empty());
429 }
430
431 #[test]
432 fn started_then_cancelled_timer_is_dead() {
433 let workflow_id = workflow_id();
434 let timer_id = named_timer_id();
435 let history = vec![
436 workflow_started_event(&workflow_id, 0),
437 timer_started_event(&workflow_id, &timer_id, 1),
438 timer_cancelled_event(&workflow_id, &timer_id, 2),
439 ];
440 assert!(live_timers_in_active_segment(&history).is_empty());
441 }
442
443 #[test]
444 fn restarted_named_timer_after_fire_is_live() {
445 let workflow_id = workflow_id();
449 let timer_id = named_timer_id();
450 let history = vec![
451 workflow_started_event(&workflow_id, 0),
452 timer_started_event(&workflow_id, &timer_id, 1),
453 timer_fired_event(&workflow_id, &timer_id, 2),
454 timer_started_event(&workflow_id, &timer_id, 3),
455 ];
456 assert_eq!(
457 live_timers_in_active_segment(&history),
458 vec![timer_id],
459 "a re-armed named timer is live again"
460 );
461 }
462
463 #[test]
464 fn restarted_named_timer_after_cancel_is_live() {
465 let workflow_id = workflow_id();
466 let timer_id = named_timer_id();
467 let history = vec![
468 workflow_started_event(&workflow_id, 0),
469 timer_started_event(&workflow_id, &timer_id, 1),
470 timer_cancelled_event(&workflow_id, &timer_id, 2),
471 timer_started_event(&workflow_id, &timer_id, 3),
472 ];
473 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
474 }
475
476 #[test]
477 fn prior_run_segment_timer_is_not_live() {
478 let workflow_id = workflow_id();
481 let prior = named_timer_id();
482 let current = make_named("current-deadline");
483 let history = vec![
484 workflow_started_event(&workflow_id, 0),
485 timer_started_event(&workflow_id, &prior, 1),
486 workflow_started_event(&workflow_id, 2),
488 timer_started_event(&workflow_id, ¤t, 3),
489 ];
490 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
491 }
492
493 #[tokio::test]
494 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
495 let process = WorkflowProcessHandle::new(42);
500 let (store, engine, service) = service();
501 let workflow_id = workflow_id();
502 let timer_id = named_timer_id();
503 let fire_at = instant(110);
504 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
505 engine.record_workflow_event(
506 &workflow_id,
507 timer_started_event(&workflow_id, &timer_id, 1),
508 )?;
509 engine
510 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
511 engine.record_workflow_event(
512 &workflow_id,
513 timer_started_event(&workflow_id, &timer_id, 3),
514 )?;
515
516 service
517 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
518 .await?;
519
520 assert_eq!(
521 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
522 2,
523 "the re-armed timer fires again, recording a second TimerFired"
524 );
525 assert_eq!(engine.delivered_messages()?.len(), 1);
526 Ok(())
527 }
528
529 #[tokio::test]
530 async fn schedule_records_timer_row_without_timer_started_event()
531 -> Result<(), TimerServiceError> {
532 let (store, _engine, service) = service();
533 let workflow_id = workflow_id();
534 let timer_id = timer_id();
535 let fire_at = instant(10);
536
537 service
538 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
539 .await?;
540
541 let expired = store.expired_timers(fire_at).await?;
542 assert_eq!(expired.len(), 1);
543 assert_eq!(expired[0].workflow_id, workflow_id);
544 assert_eq!(expired[0].timer_id, timer_id);
545 assert_eq!(expired[0].fire_at, fire_at);
546
547 assert!(history(&store, &workflow_id).await?.is_empty());
548 Ok(())
549 }
550
551 #[tokio::test]
552 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
553 let process = WorkflowProcessHandle::new(42);
554 let (_store, engine, service) = service();
555 let workflow_id = workflow_id();
556 let timer_id = timer_id();
557 let fire_at = instant(20);
558 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
559
560 service
561 .schedule(workflow_id, timer_id.clone(), fire_at)
562 .await?;
563
564 assert_eq!(
565 engine.armed_timers()?,
566 vec![TimerWheelEntry {
567 process,
568 timer_id,
569 fire_at
570 }]
571 );
572 Ok(())
573 }
574
575 #[tokio::test]
576 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
577 let (store, engine, service) = service();
578 let workflow_id = workflow_id();
579 let timer_id = timer_id();
580 let fire_at = instant(30);
581 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
582
583 service
584 .schedule(workflow_id.clone(), timer_id, fire_at)
585 .await?;
586
587 assert!(engine.armed_timers()?.is_empty());
588 assert!(history(&store, &workflow_id).await?.is_empty());
589 Ok(())
590 }
591
592 #[tokio::test]
593 async fn fire_records_timer_fired_then_delivers_mailbox_message()
594 -> Result<(), TimerServiceError> {
595 let process = WorkflowProcessHandle::new(42);
596 let (store, engine, service) = service();
597 let workflow_id = workflow_id();
598 let timer_id = timer_id();
599 let fire_at = instant(40);
600 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
601 engine.record_workflow_event(
602 &workflow_id,
603 timer_started_event(&workflow_id, &timer_id, 1),
604 )?;
605
606 service
607 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
608 .await?;
609
610 assert_eq!(
611 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
612 1
613 );
614 assert_eq!(
615 engine.delivered_messages()?,
616 vec![(
617 process,
618 DeliveredWorkflowMessage::TimerFired {
619 timer_id: timer_id.clone(),
620 fire_at
621 }
622 )]
623 );
624 assert!(matches!(
625 engine.operations()?.as_slice(),
626 [
627 FakeEngineOperation::EventRecorded {
628 event: Event::TimerStarted { .. },
629 ..
630 },
631 FakeEngineOperation::EventRecorded {
632 workflow_id: recorded_workflow_id,
633 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
634 },
635 FakeEngineOperation::Delivered {
636 process: delivered_process,
637 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
638 }
639 ] if recorded_workflow_id == &workflow_id
640 && recorded_timer_id == &timer_id
641 && delivered_process == &process
642 && delivered_timer_id == &timer_id
643 ));
644 Ok(())
645 }
646
647 #[tokio::test]
648 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
649 -> Result<(), TimerServiceError> {
650 let (store, engine, service) = service();
651 let workflow_id = workflow_id();
652 let timer_id = timer_id();
653 let fire_at = instant(50);
654 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
655 engine.record_workflow_event(
656 &workflow_id,
657 timer_started_event(&workflow_id, &timer_id, 1),
658 )?;
659
660 service
661 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
662 .await?;
663
664 assert_eq!(
665 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
666 1
667 );
668 assert!(engine.delivered_messages()?.is_empty());
669 Ok(())
670 }
671
672 #[tokio::test]
673 async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
674 let process = WorkflowProcessHandle::new(42);
675 let (store, engine, service) = service();
676 let workflow_id = workflow_id();
677 let timer_id = timer_id();
678 let fire_at = instant(60);
679 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
680 engine.record_workflow_event(
681 &workflow_id,
682 timer_started_event(&workflow_id, &timer_id, 1),
683 )?;
684
685 service
686 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
687 .await?;
688 service
689 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
690 .await?;
691
692 assert_eq!(
693 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
694 1
695 );
696 assert_eq!(engine.delivered_messages()?.len(), 1);
697 Ok(())
698 }
699
700 #[tokio::test]
701 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
702 let process = WorkflowProcessHandle::new(42);
703 let (store, engine, service) = service();
704 let workflow_id = workflow_id();
705 let timer_id = timer_id();
706 let fire_at = instant(70);
707 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
708 engine.record_workflow_event(
709 &workflow_id,
710 timer_started_event(&workflow_id, &timer_id, 1),
711 )?;
712 let cancelled = Event::TimerCancelled {
713 envelope: EventEnvelope {
714 seq: 2,
715 recorded_at: instant(69),
716 workflow_id: workflow_id.clone(),
717 },
718 timer_id: timer_id.clone(),
719 };
720 engine.record_workflow_event(&workflow_id, cancelled)?;
721
722 service
723 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
724 .await?;
725
726 let history = history(&store, &workflow_id).await?;
727 assert_eq!(count_timer_fired(&history, &timer_id), 0);
728 assert!(engine.delivered_messages()?.is_empty());
729 Ok(())
730 }
731
732 #[tokio::test]
733 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
734 let process = WorkflowProcessHandle::new(42);
735 let (store, engine, service) = service();
736 let workflow_id = workflow_id();
737 let timer_id = timer_id();
738 let fire_at = instant(80);
739
740 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
741 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
742 engine.record_workflow_event(
743 &workflow_id,
744 timer_started_event(&workflow_id, &timer_id, 1),
745 )?;
746 service
747 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
748 .await?;
749
750 assert_eq!(
751 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
752 1
753 );
754 assert!(engine.delivered_messages()?.is_empty());
755 Ok(())
756 }
757
758 #[tokio::test]
759 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
760 let process = WorkflowProcessHandle::new(42);
761 let (store, engine, service) = service();
762 let workflow_id = workflow_id();
763 let timer_id = timer_id();
764 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
765
766 service
767 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
768 .await?;
769
770 assert!(history(&store, &workflow_id).await?.is_empty());
771 assert!(engine.delivered_messages()?.is_empty());
772 Ok(())
773 }
774
775 #[tokio::test]
776 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
777 {
778 let process = WorkflowProcessHandle::new(42);
779 let (store, engine, service) = service();
780 let workflow_id = workflow_id();
781 let timer_id = timer_id();
782 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
783 engine.record_workflow_event(
785 &workflow_id,
786 timer_started_event(&workflow_id, &timer_id, 1),
787 )?;
788 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
789
790 service
791 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
792 .await?;
793
794 assert_eq!(
795 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
796 0
797 );
798 assert!(engine.delivered_messages()?.is_empty());
799 Ok(())
800 }
801}