1use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, TimerCancelCause, TimerId, WorkflowId};
6use aion_store::{ReadableEventStore, StoreError};
7use chrono::{DateTime, Utc};
8use dashmap::DashSet;
9
10use crate::engine_seam::{
11 EngineHandle, EngineSeamError, RecordOutcome, TimerWheelEntry, WorkflowMailboxMessage,
12 WorkflowResidency,
13};
14use crate::time::deadline::{DeadlineHandler, deadline_run_id, is_deadline_timer};
15
16pub struct TimerService {
22 engine: Arc<dyn EngineHandle>,
23 store: Arc<dyn ReadableEventStore>,
24 recorded_at: fn() -> DateTime<Utc>,
25 terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
33 deadline_handler: Option<Arc<dyn DeadlineHandler>>,
39}
40
41struct TerminalUpdateSlot<'a> {
42 terminal_updates: &'a DashSet<(WorkflowId, TimerId)>,
43 key: (WorkflowId, TimerId),
44}
45
46impl Drop for TerminalUpdateSlot<'_> {
47 fn drop(&mut self) {
48 self.terminal_updates.remove(&self.key);
49 }
50}
51
52#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
54pub enum TimerServiceError {
55 #[error("timer store operation failed: {0}")]
57 Store(#[from] StoreError),
58
59 #[error("timer engine operation failed: {0}")]
61 Engine(#[from] EngineSeamError),
62
63 #[error("deadline timer routing failed: {0}")]
71 Deadline(String),
72}
73
74impl TimerService {
75 #[must_use]
77 pub fn new(engine: Arc<dyn EngineHandle>, store: Arc<dyn ReadableEventStore>) -> Self {
78 Self::with_recorded_at(engine, store, Utc::now)
79 }
80
81 #[must_use]
83 pub fn with_recorded_at(
84 engine: Arc<dyn EngineHandle>,
85 store: Arc<dyn ReadableEventStore>,
86 recorded_at: fn() -> DateTime<Utc>,
87 ) -> Self {
88 Self {
89 engine,
90 store,
91 recorded_at,
92 terminal_updates: Arc::new(DashSet::new()),
93 deadline_handler: None,
94 }
95 }
96
97 #[must_use]
105 pub fn with_terminal_updates(
106 mut self,
107 terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
108 ) -> Self {
109 self.terminal_updates = terminal_updates;
110 self
111 }
112
113 #[must_use]
120 pub fn with_deadline_handler(mut self, handler: Arc<dyn DeadlineHandler>) -> Self {
121 self.deadline_handler = Some(handler);
122 self
123 }
124
125 pub async fn schedule(
136 &self,
137 workflow_id: WorkflowId,
138 timer_id: TimerId,
139 fire_at: DateTime<Utc>,
140 ) -> Result<(), TimerServiceError> {
141 self.store
142 .schedule_timer(&workflow_id, &timer_id, fire_at)
143 .await?;
144
145 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
146 self.engine.arm_timer(TimerWheelEntry {
147 process,
148 timer_id,
149 fire_at,
150 })?;
151 }
152
153 Ok(())
154 }
155
156 pub async fn cancel(
173 &self,
174 workflow_id: WorkflowId,
175 timer_id: TimerId,
176 cause: TimerCancelCause,
177 ) -> Result<(), TimerServiceError> {
178 let key = (workflow_id.clone(), timer_id.clone());
179 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
180
181 let result = self.cancel_guarded(workflow_id, timer_id, cause).await;
182 drop(terminal_update_slot);
183 result
184 }
185
186 async fn cancel_guarded(
187 &self,
188 workflow_id: WorkflowId,
189 timer_id: TimerId,
190 cause: TimerCancelCause,
191 ) -> Result<(), TimerServiceError> {
192 if !self.timer_is_live(&workflow_id, &timer_id).await? {
193 return Ok(());
194 }
195
196 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
197 self.engine.disarm_timer(process, &timer_id)?;
198 }
199
200 let event = Event::TimerCancelled {
201 envelope: self.next_envelope(&workflow_id).await?,
202 timer_id,
203 cause,
204 };
205 self.engine.record_workflow_event(&workflow_id, event)?;
206
207 Ok(())
208 }
209
210 pub async fn fire_timer(
220 &self,
221 workflow_id: WorkflowId,
222 timer_id: TimerId,
223 fire_at: DateTime<Utc>,
224 ) -> Result<(), TimerServiceError> {
225 let key = (workflow_id.clone(), timer_id.clone());
226 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
227
228 let result = self
229 .fire_timer_guarded(workflow_id, timer_id, fire_at)
230 .await;
231 drop(terminal_update_slot);
232 result
233 }
234
235 async fn wait_for_terminal_update_slot(
236 &self,
237 key: (WorkflowId, TimerId),
238 ) -> TerminalUpdateSlot<'_> {
239 loop {
240 if self.terminal_updates.insert(key.clone()) {
241 return TerminalUpdateSlot {
242 terminal_updates: self.terminal_updates.as_ref(),
243 key,
244 };
245 }
246 tokio::task::yield_now().await;
247 }
248 }
249
250 async fn fire_timer_guarded(
251 &self,
252 workflow_id: WorkflowId,
253 timer_id: TimerId,
254 fire_at: DateTime<Utc>,
255 ) -> Result<(), TimerServiceError> {
256 if !self.timer_is_live(&workflow_id, &timer_id).await? {
257 return Ok(());
258 }
259
260 if is_deadline_timer(&timer_id) {
265 return self.fire_deadline(workflow_id, timer_id).await;
266 }
267
268 let event = Event::TimerFired {
269 envelope: self.next_envelope(&workflow_id).await?,
270 timer_id: timer_id.clone(),
271 };
272 if self.engine.record_workflow_event(&workflow_id, event)? == RecordOutcome::RefusedTerminal
278 {
279 return Ok(());
280 }
281
282 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
283 self.engine.deliver_workflow_message(
284 process,
285 WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
286 )?;
287 }
288
289 Ok(())
290 }
291
292 async fn fire_deadline(
301 &self,
302 workflow_id: WorkflowId,
303 timer_id: TimerId,
304 ) -> Result<(), TimerServiceError> {
305 let handler = self.deadline_handler.as_ref().ok_or_else(|| {
306 TimerServiceError::Deadline(format!(
307 "no deadline handler registered for {timer_id} on workflow {workflow_id}"
308 ))
309 })?;
310 let run_id = deadline_run_id(&timer_id).ok_or_else(|| {
311 TimerServiceError::Deadline(format!(
312 "malformed deadline timer {timer_id} on workflow {workflow_id}"
313 ))
314 })?;
315 handler
316 .on_deadline_elapsed(workflow_id, run_id)
317 .await
318 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
319 }
320
321 async fn timer_is_live(
336 &self,
337 workflow_id: &WorkflowId,
338 timer_id: &TimerId,
339 ) -> Result<bool, StoreError> {
340 let history = self.store.read_history(workflow_id).await?;
341 Ok(live_timers_in_active_segment(&history).contains(timer_id))
342 }
343
344 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
345 let history = self.store.read_history(workflow_id).await?;
346 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
347 Ok(EventEnvelope {
348 seq,
349 recorded_at: (self.recorded_at)(),
350 workflow_id: workflow_id.clone(),
351 })
352 }
353}
354
355pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
370 let segment_start = history
371 .iter()
372 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
373 .unwrap_or(0);
374 let mut live: Vec<TimerId> = Vec::new();
375 for event in &history[segment_start..] {
376 match event {
377 Event::TimerStarted { timer_id, .. } if !live.contains(timer_id) => {
378 live.push(timer_id.clone());
379 }
380 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
381 live.retain(|id| id != timer_id);
382 }
383 _ => {}
384 }
385 }
386 live
387}
388
389#[cfg(test)]
390mod tests {
391 use std::sync::Arc;
392
393 use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
394 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
395 use chrono::{DateTime, Utc};
396
397 use super::{TimerService, TimerServiceError, live_timers_in_active_segment};
398 use crate::engine_seam::test_support::{
399 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
400 };
401 use crate::engine_seam::{
402 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
403 };
404 use crate::time::deadline::{DeadlineHandler, DeadlineHandlerError, deadline_timer_id};
405
406 fn instant(offset_seconds: i64) -> DateTime<Utc> {
407 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
408 }
409
410 fn workflow_id() -> WorkflowId {
411 WorkflowId::new_v4()
412 }
413
414 fn timer_id() -> TimerId {
415 TimerId::anonymous(7)
416 }
417
418 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
419 let concrete_store = Arc::new(InMemoryStore::default());
420 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
421 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
422 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
423 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
424 (concrete_store, engine, service)
425 }
426
427 fn recorded_at() -> DateTime<Utc> {
428 instant(1)
429 }
430
431 async fn history(
432 store: &InMemoryStore,
433 workflow_id: &WorkflowId,
434 ) -> Result<Vec<Event>, StoreError> {
435 store.read_history(workflow_id).await
436 }
437
438 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
439 events
440 .iter()
441 .filter(|event| {
442 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
443 })
444 .count()
445 }
446
447 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
448 Event::TimerStarted {
449 envelope: EventEnvelope {
450 seq,
451 recorded_at: instant(0),
452 workflow_id: workflow_id.clone(),
453 },
454 timer_id: timer_id.clone(),
455 fire_at: instant(5),
456 }
457 }
458
459 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
460 Event::WorkflowStarted {
461 envelope: EventEnvelope {
462 seq,
463 recorded_at: instant(0),
464 workflow_id: workflow_id.clone(),
465 },
466 workflow_type: "fixture".to_owned(),
467 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
468 run_id: aion_core::RunId::new_v4(),
469 parent_run_id: None,
470 package_version: aion_core::PackageVersion::new("a".repeat(64)),
471 }
472 }
473
474 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
475 Event::TimerFired {
476 envelope: EventEnvelope {
477 seq,
478 recorded_at: instant(0),
479 workflow_id: workflow_id.clone(),
480 },
481 timer_id: timer_id.clone(),
482 }
483 }
484
485 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
486 Event::TimerCancelled {
487 cause: TimerCancelCause::WorkflowIntent,
488 envelope: EventEnvelope {
489 seq,
490 recorded_at: instant(0),
491 workflow_id: workflow_id.clone(),
492 },
493 timer_id: timer_id.clone(),
494 }
495 }
496
497 fn make_named(name: &str) -> TimerId {
498 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
502 }
503
504 fn named_timer_id() -> TimerId {
505 make_named("review-deadline")
506 }
507
508 #[test]
511 fn started_timer_is_live() {
512 let workflow_id = workflow_id();
513 let timer_id = named_timer_id();
514 let history = vec![
515 workflow_started_event(&workflow_id, 0),
516 timer_started_event(&workflow_id, &timer_id, 1),
517 ];
518 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
519 }
520
521 #[test]
522 fn started_then_fired_timer_is_dead() {
523 let workflow_id = workflow_id();
524 let timer_id = named_timer_id();
525 let history = vec![
526 workflow_started_event(&workflow_id, 0),
527 timer_started_event(&workflow_id, &timer_id, 1),
528 timer_fired_event(&workflow_id, &timer_id, 2),
529 ];
530 assert!(live_timers_in_active_segment(&history).is_empty());
531 }
532
533 #[test]
534 fn started_then_cancelled_timer_is_dead() {
535 let workflow_id = workflow_id();
536 let timer_id = named_timer_id();
537 let history = vec![
538 workflow_started_event(&workflow_id, 0),
539 timer_started_event(&workflow_id, &timer_id, 1),
540 timer_cancelled_event(&workflow_id, &timer_id, 2),
541 ];
542 assert!(live_timers_in_active_segment(&history).is_empty());
543 }
544
545 #[test]
546 fn restarted_named_timer_after_fire_is_live() {
547 let workflow_id = workflow_id();
551 let timer_id = named_timer_id();
552 let history = vec![
553 workflow_started_event(&workflow_id, 0),
554 timer_started_event(&workflow_id, &timer_id, 1),
555 timer_fired_event(&workflow_id, &timer_id, 2),
556 timer_started_event(&workflow_id, &timer_id, 3),
557 ];
558 assert_eq!(
559 live_timers_in_active_segment(&history),
560 vec![timer_id],
561 "a re-armed named timer is live again"
562 );
563 }
564
565 #[test]
566 fn restarted_named_timer_after_cancel_is_live() {
567 let workflow_id = workflow_id();
568 let timer_id = named_timer_id();
569 let history = vec![
570 workflow_started_event(&workflow_id, 0),
571 timer_started_event(&workflow_id, &timer_id, 1),
572 timer_cancelled_event(&workflow_id, &timer_id, 2),
573 timer_started_event(&workflow_id, &timer_id, 3),
574 ];
575 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
576 }
577
578 #[test]
579 fn prior_run_segment_timer_is_not_live() {
580 let workflow_id = workflow_id();
583 let prior = named_timer_id();
584 let current = make_named("current-deadline");
585 let history = vec![
586 workflow_started_event(&workflow_id, 0),
587 timer_started_event(&workflow_id, &prior, 1),
588 workflow_started_event(&workflow_id, 2),
590 timer_started_event(&workflow_id, ¤t, 3),
591 ];
592 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
593 }
594
595 #[tokio::test]
596 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
597 let process = WorkflowProcessHandle::new(42);
602 let (store, engine, service) = service();
603 let workflow_id = workflow_id();
604 let timer_id = named_timer_id();
605 let fire_at = instant(110);
606 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
607 engine.record_workflow_event(
608 &workflow_id,
609 timer_started_event(&workflow_id, &timer_id, 1),
610 )?;
611 engine
612 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
613 engine.record_workflow_event(
614 &workflow_id,
615 timer_started_event(&workflow_id, &timer_id, 3),
616 )?;
617
618 service
619 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
620 .await?;
621
622 assert_eq!(
623 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
624 2,
625 "the re-armed timer fires again, recording a second TimerFired"
626 );
627 assert_eq!(engine.delivered_messages()?.len(), 1);
628 Ok(())
629 }
630
631 #[tokio::test]
632 async fn schedule_records_timer_row_without_timer_started_event()
633 -> Result<(), TimerServiceError> {
634 let (store, _engine, service) = service();
635 let workflow_id = workflow_id();
636 let timer_id = timer_id();
637 let fire_at = instant(10);
638
639 service
640 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
641 .await?;
642
643 let expired = store.expired_timers(fire_at).await?;
644 assert_eq!(expired.len(), 1);
645 assert_eq!(expired[0].workflow_id, workflow_id);
646 assert_eq!(expired[0].timer_id, timer_id);
647 assert_eq!(expired[0].fire_at, fire_at);
648
649 assert!(history(&store, &workflow_id).await?.is_empty());
650 Ok(())
651 }
652
653 #[tokio::test]
654 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
655 let process = WorkflowProcessHandle::new(42);
656 let (_store, engine, service) = service();
657 let workflow_id = workflow_id();
658 let timer_id = timer_id();
659 let fire_at = instant(20);
660 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
661
662 service
663 .schedule(workflow_id, timer_id.clone(), fire_at)
664 .await?;
665
666 assert_eq!(
667 engine.armed_timers()?,
668 vec![TimerWheelEntry {
669 process,
670 timer_id,
671 fire_at
672 }]
673 );
674 Ok(())
675 }
676
677 #[tokio::test]
678 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
679 let (store, engine, service) = service();
680 let workflow_id = workflow_id();
681 let timer_id = timer_id();
682 let fire_at = instant(30);
683 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
684
685 service
686 .schedule(workflow_id.clone(), timer_id, fire_at)
687 .await?;
688
689 assert!(engine.armed_timers()?.is_empty());
690 assert!(history(&store, &workflow_id).await?.is_empty());
691 Ok(())
692 }
693
694 #[tokio::test]
695 async fn fire_records_timer_fired_then_delivers_mailbox_message()
696 -> Result<(), TimerServiceError> {
697 let process = WorkflowProcessHandle::new(42);
698 let (store, engine, service) = service();
699 let workflow_id = workflow_id();
700 let timer_id = timer_id();
701 let fire_at = instant(40);
702 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
703 engine.record_workflow_event(
704 &workflow_id,
705 timer_started_event(&workflow_id, &timer_id, 1),
706 )?;
707
708 service
709 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
710 .await?;
711
712 assert_eq!(
713 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
714 1
715 );
716 assert_eq!(
717 engine.delivered_messages()?,
718 vec![(
719 process,
720 DeliveredWorkflowMessage::TimerFired {
721 timer_id: timer_id.clone(),
722 fire_at
723 }
724 )]
725 );
726 assert!(matches!(
727 engine.operations()?.as_slice(),
728 [
729 FakeEngineOperation::EventRecorded {
730 event: Event::TimerStarted { .. },
731 ..
732 },
733 FakeEngineOperation::EventRecorded {
734 workflow_id: recorded_workflow_id,
735 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
736 },
737 FakeEngineOperation::Delivered {
738 process: delivered_process,
739 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
740 }
741 ] if recorded_workflow_id == &workflow_id
742 && recorded_timer_id == &timer_id
743 && delivered_process == &process
744 && delivered_timer_id == &timer_id
745 ));
746 Ok(())
747 }
748
749 #[tokio::test]
750 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
751 -> Result<(), TimerServiceError> {
752 let (store, engine, service) = service();
753 let workflow_id = workflow_id();
754 let timer_id = timer_id();
755 let fire_at = instant(50);
756 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
757 engine.record_workflow_event(
758 &workflow_id,
759 timer_started_event(&workflow_id, &timer_id, 1),
760 )?;
761
762 service
763 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
764 .await?;
765
766 assert_eq!(
767 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
768 1
769 );
770 assert!(engine.delivered_messages()?.is_empty());
771 Ok(())
772 }
773
774 #[tokio::test]
775 async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
776 let process = WorkflowProcessHandle::new(42);
777 let (store, engine, service) = service();
778 let workflow_id = workflow_id();
779 let timer_id = timer_id();
780 let fire_at = instant(60);
781 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
782 engine.record_workflow_event(
783 &workflow_id,
784 timer_started_event(&workflow_id, &timer_id, 1),
785 )?;
786
787 service
788 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
789 .await?;
790 service
791 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
792 .await?;
793
794 assert_eq!(
795 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
796 1
797 );
798 assert_eq!(engine.delivered_messages()?.len(), 1);
799 Ok(())
800 }
801
802 #[tokio::test]
803 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
804 let process = WorkflowProcessHandle::new(42);
805 let (store, engine, service) = service();
806 let workflow_id = workflow_id();
807 let timer_id = timer_id();
808 let fire_at = instant(70);
809 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
810 engine.record_workflow_event(
811 &workflow_id,
812 timer_started_event(&workflow_id, &timer_id, 1),
813 )?;
814 let cancelled = Event::TimerCancelled {
815 cause: TimerCancelCause::WorkflowIntent,
816 envelope: EventEnvelope {
817 seq: 2,
818 recorded_at: instant(69),
819 workflow_id: workflow_id.clone(),
820 },
821 timer_id: timer_id.clone(),
822 };
823 engine.record_workflow_event(&workflow_id, cancelled)?;
824
825 service
826 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
827 .await?;
828
829 let history = history(&store, &workflow_id).await?;
830 assert_eq!(count_timer_fired(&history, &timer_id), 0);
831 assert!(engine.delivered_messages()?.is_empty());
832 Ok(())
833 }
834
835 #[tokio::test]
836 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
837 let process = WorkflowProcessHandle::new(42);
838 let (store, engine, service) = service();
839 let workflow_id = workflow_id();
840 let timer_id = timer_id();
841 let fire_at = instant(80);
842
843 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
844 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
845 engine.record_workflow_event(
846 &workflow_id,
847 timer_started_event(&workflow_id, &timer_id, 1),
848 )?;
849 service
850 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
851 .await?;
852
853 assert_eq!(
854 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
855 1
856 );
857 assert!(engine.delivered_messages()?.is_empty());
858 Ok(())
859 }
860
861 #[tokio::test]
862 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
863 let process = WorkflowProcessHandle::new(42);
864 let (store, engine, service) = service();
865 let workflow_id = workflow_id();
866 let timer_id = timer_id();
867 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
868
869 service
870 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
871 .await?;
872
873 assert!(history(&store, &workflow_id).await?.is_empty());
874 assert!(engine.delivered_messages()?.is_empty());
875 Ok(())
876 }
877
878 struct RecordingDeadlineHandler {
880 calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
881 fail: bool,
882 }
883
884 impl RecordingDeadlineHandler {
885 fn new(fail: bool) -> Self {
886 Self {
887 calls: std::sync::Mutex::new(Vec::new()),
888 fail,
889 }
890 }
891
892 fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
893 self.calls
894 .lock()
895 .map(|calls| calls.clone())
896 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
897 }
898 }
899
900 #[async_trait::async_trait]
901 impl DeadlineHandler for RecordingDeadlineHandler {
902 async fn on_deadline_elapsed(
903 &self,
904 workflow_id: WorkflowId,
905 run_id: RunId,
906 ) -> Result<(), DeadlineHandlerError> {
907 self.calls
908 .lock()
909 .map_err(|error| DeadlineHandlerError(error.to_string()))?
910 .push((workflow_id, run_id));
911 if self.fail {
912 Err(DeadlineHandlerError(
913 "deliberate handler failure".to_owned(),
914 ))
915 } else {
916 Ok(())
917 }
918 }
919 }
920
921 fn service_with_handler(
922 handler: Arc<dyn DeadlineHandler>,
923 ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
924 let concrete_store = Arc::new(InMemoryStore::default());
925 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
926 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
927 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
928 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
929 .with_deadline_handler(handler);
930 (concrete_store, engine, service)
931 }
932
933 #[tokio::test]
936 async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
937 -> Result<(), TimerServiceError> {
938 let run_id = RunId::new_v4();
939 let deadline_id = deadline_timer_id(&run_id)
940 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
941 let handler = Arc::new(RecordingDeadlineHandler::new(false));
942 let (store, engine, service) = service_with_handler(handler.clone());
943 let workflow_id = workflow_id();
944 let fire_at = instant(120);
945 engine.set_residency(
946 workflow_id.clone(),
947 WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
948 )?;
949 engine.record_workflow_event(
950 &workflow_id,
951 timer_started_event(&workflow_id, &deadline_id, 1),
952 )?;
953
954 service
955 .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
956 .await?;
957
958 assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
959 assert_eq!(
960 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
961 0,
962 "a deadline fire never records TimerFired"
963 );
964 assert!(engine.delivered_messages()?.is_empty());
965 Ok(())
966 }
967
968 #[tokio::test]
971 async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
972 let run_id = RunId::new_v4();
973 let deadline_id = deadline_timer_id(&run_id)
974 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
975 let (store, engine, service) = service();
976 let workflow_id = workflow_id();
977 engine.record_workflow_event(
978 &workflow_id,
979 timer_started_event(&workflow_id, &deadline_id, 1),
980 )?;
981
982 let result = service
983 .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
984 .await;
985
986 assert!(
987 matches!(result, Err(TimerServiceError::Deadline(_))),
988 "unhandled deadline fire must be a typed error, got {result:?}"
989 );
990 assert_eq!(
991 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
992 0
993 );
994 Ok(())
995 }
996
997 #[tokio::test]
999 async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1000 let run_id = RunId::new_v4();
1001 let deadline_id = deadline_timer_id(&run_id)
1002 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1003 let handler = Arc::new(RecordingDeadlineHandler::new(true));
1004 let (_store, engine, service) = service_with_handler(handler);
1005 let workflow_id = workflow_id();
1006 engine.record_workflow_event(
1007 &workflow_id,
1008 timer_started_event(&workflow_id, &deadline_id, 1),
1009 )?;
1010
1011 let result = service
1012 .fire_timer(workflow_id, deadline_id, instant(120))
1013 .await;
1014
1015 assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1016 Ok(())
1017 }
1018
1019 #[tokio::test]
1025 async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1026 -> Result<(), TimerServiceError> {
1027 let process = WorkflowProcessHandle::new(42);
1028 let (store, engine, service) = service();
1029 let workflow_id = workflow_id();
1030 let timer_id = timer_id();
1031 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1032 engine.record_workflow_event(
1033 &workflow_id,
1034 timer_started_event(&workflow_id, &timer_id, 1),
1035 )?;
1036 engine.refuse_next_record_as_terminal()?;
1037
1038 service
1039 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1040 .await?;
1041
1042 assert_eq!(
1043 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1044 0,
1045 "a refused fire records no TimerFired"
1046 );
1047 assert!(
1048 engine.delivered_messages()?.is_empty(),
1049 "a refused fire delivers no wake"
1050 );
1051 Ok(())
1052 }
1053
1054 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1062 async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1063 -> Result<(), TimerServiceError> {
1064 use dashmap::DashSet;
1065 use tokio::sync::Barrier;
1066
1067 for _ in 0..20 {
1068 let process = WorkflowProcessHandle::new(42);
1069 let concrete_store = Arc::new(InMemoryStore::default());
1070 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1071 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1072 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1073 let coordinator = Arc::new(DashSet::new());
1074 let service_a =
1075 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1076 .with_terminal_updates(Arc::clone(&coordinator));
1077 let service_b =
1078 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1079 .with_terminal_updates(Arc::clone(&coordinator));
1080
1081 let workflow_id = workflow_id();
1082 let timer_id = timer_id();
1083 let fire_at = instant(200);
1084 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1085 engine.record_workflow_event(
1086 &workflow_id,
1087 timer_started_event(&workflow_id, &timer_id, 1),
1088 )?;
1089
1090 let gate = Arc::new(Barrier::new(2));
1091 let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1092 let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1093 let cancel = async move {
1094 cancel_gate.wait().await;
1095 service_a
1096 .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1097 .await
1098 };
1099 let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1100 let fire = async move {
1101 fire_gate.wait().await;
1102 service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1103 };
1104 let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1105 cancel_result?;
1106 fire_result?;
1107
1108 let history = history(&concrete_store, &workflow_id).await?;
1109 let terminal_timer_events = history
1110 .iter()
1111 .filter(|event| {
1112 matches!(
1113 event,
1114 Event::TimerFired { timer_id: recorded, .. }
1115 | Event::TimerCancelled { timer_id: recorded, .. }
1116 if recorded == &timer_id
1117 )
1118 })
1119 .count();
1120 assert_eq!(
1121 terminal_timer_events, 1,
1122 "first-recorded wins across shared services: {history:#?}"
1123 );
1124 }
1125 Ok(())
1126 }
1127
1128 #[tokio::test]
1129 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1130 {
1131 let process = WorkflowProcessHandle::new(42);
1132 let (store, engine, service) = service();
1133 let workflow_id = workflow_id();
1134 let timer_id = timer_id();
1135 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1136 engine.record_workflow_event(
1138 &workflow_id,
1139 timer_started_event(&workflow_id, &timer_id, 1),
1140 )?;
1141 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1142
1143 service
1144 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1145 .await?;
1146
1147 assert_eq!(
1148 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1149 0
1150 );
1151 assert!(engine.delivered_messages()?.is_empty());
1152 Ok(())
1153 }
1154}