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 parent_workflow_id: None,
471 package_version: aion_core::PackageVersion::new("a".repeat(64)),
472 }
473 }
474
475 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
476 Event::TimerFired {
477 envelope: EventEnvelope {
478 seq,
479 recorded_at: instant(0),
480 workflow_id: workflow_id.clone(),
481 },
482 timer_id: timer_id.clone(),
483 }
484 }
485
486 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
487 Event::TimerCancelled {
488 cause: TimerCancelCause::WorkflowIntent,
489 envelope: EventEnvelope {
490 seq,
491 recorded_at: instant(0),
492 workflow_id: workflow_id.clone(),
493 },
494 timer_id: timer_id.clone(),
495 }
496 }
497
498 fn make_named(name: &str) -> TimerId {
499 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
503 }
504
505 fn named_timer_id() -> TimerId {
506 make_named("review-deadline")
507 }
508
509 #[test]
512 fn started_timer_is_live() {
513 let workflow_id = workflow_id();
514 let timer_id = named_timer_id();
515 let history = vec![
516 workflow_started_event(&workflow_id, 0),
517 timer_started_event(&workflow_id, &timer_id, 1),
518 ];
519 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
520 }
521
522 #[test]
523 fn started_then_fired_timer_is_dead() {
524 let workflow_id = workflow_id();
525 let timer_id = named_timer_id();
526 let history = vec![
527 workflow_started_event(&workflow_id, 0),
528 timer_started_event(&workflow_id, &timer_id, 1),
529 timer_fired_event(&workflow_id, &timer_id, 2),
530 ];
531 assert!(live_timers_in_active_segment(&history).is_empty());
532 }
533
534 #[test]
535 fn started_then_cancelled_timer_is_dead() {
536 let workflow_id = workflow_id();
537 let timer_id = named_timer_id();
538 let history = vec![
539 workflow_started_event(&workflow_id, 0),
540 timer_started_event(&workflow_id, &timer_id, 1),
541 timer_cancelled_event(&workflow_id, &timer_id, 2),
542 ];
543 assert!(live_timers_in_active_segment(&history).is_empty());
544 }
545
546 #[test]
547 fn restarted_named_timer_after_fire_is_live() {
548 let workflow_id = workflow_id();
552 let timer_id = named_timer_id();
553 let history = vec![
554 workflow_started_event(&workflow_id, 0),
555 timer_started_event(&workflow_id, &timer_id, 1),
556 timer_fired_event(&workflow_id, &timer_id, 2),
557 timer_started_event(&workflow_id, &timer_id, 3),
558 ];
559 assert_eq!(
560 live_timers_in_active_segment(&history),
561 vec![timer_id],
562 "a re-armed named timer is live again"
563 );
564 }
565
566 #[test]
567 fn restarted_named_timer_after_cancel_is_live() {
568 let workflow_id = workflow_id();
569 let timer_id = named_timer_id();
570 let history = vec![
571 workflow_started_event(&workflow_id, 0),
572 timer_started_event(&workflow_id, &timer_id, 1),
573 timer_cancelled_event(&workflow_id, &timer_id, 2),
574 timer_started_event(&workflow_id, &timer_id, 3),
575 ];
576 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
577 }
578
579 #[test]
580 fn prior_run_segment_timer_is_not_live() {
581 let workflow_id = workflow_id();
584 let prior = named_timer_id();
585 let current = make_named("current-deadline");
586 let history = vec![
587 workflow_started_event(&workflow_id, 0),
588 timer_started_event(&workflow_id, &prior, 1),
589 workflow_started_event(&workflow_id, 2),
591 timer_started_event(&workflow_id, ¤t, 3),
592 ];
593 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
594 }
595
596 #[tokio::test]
597 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
598 let process = WorkflowProcessHandle::new(42);
603 let (store, engine, service) = service();
604 let workflow_id = workflow_id();
605 let timer_id = named_timer_id();
606 let fire_at = instant(110);
607 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
608 engine.record_workflow_event(
609 &workflow_id,
610 timer_started_event(&workflow_id, &timer_id, 1),
611 )?;
612 engine
613 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
614 engine.record_workflow_event(
615 &workflow_id,
616 timer_started_event(&workflow_id, &timer_id, 3),
617 )?;
618
619 service
620 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
621 .await?;
622
623 assert_eq!(
624 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
625 2,
626 "the re-armed timer fires again, recording a second TimerFired"
627 );
628 assert_eq!(engine.delivered_messages()?.len(), 1);
629 Ok(())
630 }
631
632 #[tokio::test]
633 async fn schedule_records_timer_row_without_timer_started_event()
634 -> Result<(), TimerServiceError> {
635 let (store, _engine, service) = service();
636 let workflow_id = workflow_id();
637 let timer_id = timer_id();
638 let fire_at = instant(10);
639
640 service
641 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
642 .await?;
643
644 let expired = store.expired_timers(fire_at).await?;
645 assert_eq!(expired.len(), 1);
646 assert_eq!(expired[0].workflow_id, workflow_id);
647 assert_eq!(expired[0].timer_id, timer_id);
648 assert_eq!(expired[0].fire_at, fire_at);
649
650 assert!(history(&store, &workflow_id).await?.is_empty());
651 Ok(())
652 }
653
654 #[tokio::test]
655 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
656 let process = WorkflowProcessHandle::new(42);
657 let (_store, engine, service) = service();
658 let workflow_id = workflow_id();
659 let timer_id = timer_id();
660 let fire_at = instant(20);
661 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
662
663 service
664 .schedule(workflow_id, timer_id.clone(), fire_at)
665 .await?;
666
667 assert_eq!(
668 engine.armed_timers()?,
669 vec![TimerWheelEntry {
670 process,
671 timer_id,
672 fire_at
673 }]
674 );
675 Ok(())
676 }
677
678 #[tokio::test]
679 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
680 let (store, engine, service) = service();
681 let workflow_id = workflow_id();
682 let timer_id = timer_id();
683 let fire_at = instant(30);
684 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
685
686 service
687 .schedule(workflow_id.clone(), timer_id, fire_at)
688 .await?;
689
690 assert!(engine.armed_timers()?.is_empty());
691 assert!(history(&store, &workflow_id).await?.is_empty());
692 Ok(())
693 }
694
695 #[tokio::test]
696 async fn fire_records_timer_fired_then_delivers_mailbox_message()
697 -> Result<(), TimerServiceError> {
698 let process = WorkflowProcessHandle::new(42);
699 let (store, engine, service) = service();
700 let workflow_id = workflow_id();
701 let timer_id = timer_id();
702 let fire_at = instant(40);
703 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
704 engine.record_workflow_event(
705 &workflow_id,
706 timer_started_event(&workflow_id, &timer_id, 1),
707 )?;
708
709 service
710 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
711 .await?;
712
713 assert_eq!(
714 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
715 1
716 );
717 assert_eq!(
718 engine.delivered_messages()?,
719 vec![(
720 process,
721 DeliveredWorkflowMessage::TimerFired {
722 timer_id: timer_id.clone(),
723 fire_at
724 }
725 )]
726 );
727 assert!(matches!(
728 engine.operations()?.as_slice(),
729 [
730 FakeEngineOperation::EventRecorded {
731 event: Event::TimerStarted { .. },
732 ..
733 },
734 FakeEngineOperation::EventRecorded {
735 workflow_id: recorded_workflow_id,
736 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
737 },
738 FakeEngineOperation::Delivered {
739 process: delivered_process,
740 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
741 }
742 ] if recorded_workflow_id == &workflow_id
743 && recorded_timer_id == &timer_id
744 && delivered_process == &process
745 && delivered_timer_id == &timer_id
746 ));
747 Ok(())
748 }
749
750 #[tokio::test]
751 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
752 -> Result<(), TimerServiceError> {
753 let (store, engine, service) = service();
754 let workflow_id = workflow_id();
755 let timer_id = timer_id();
756 let fire_at = instant(50);
757 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
758 engine.record_workflow_event(
759 &workflow_id,
760 timer_started_event(&workflow_id, &timer_id, 1),
761 )?;
762
763 service
764 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
765 .await?;
766
767 assert_eq!(
768 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
769 1
770 );
771 assert!(engine.delivered_messages()?.is_empty());
772 Ok(())
773 }
774
775 #[tokio::test]
776 async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
777 let process = WorkflowProcessHandle::new(42);
778 let (store, engine, service) = service();
779 let workflow_id = workflow_id();
780 let timer_id = timer_id();
781 let fire_at = instant(60);
782 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
783 engine.record_workflow_event(
784 &workflow_id,
785 timer_started_event(&workflow_id, &timer_id, 1),
786 )?;
787
788 service
789 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
790 .await?;
791 service
792 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
793 .await?;
794
795 assert_eq!(
796 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
797 1
798 );
799 assert_eq!(engine.delivered_messages()?.len(), 1);
800 Ok(())
801 }
802
803 #[tokio::test]
804 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
805 let process = WorkflowProcessHandle::new(42);
806 let (store, engine, service) = service();
807 let workflow_id = workflow_id();
808 let timer_id = timer_id();
809 let fire_at = instant(70);
810 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
811 engine.record_workflow_event(
812 &workflow_id,
813 timer_started_event(&workflow_id, &timer_id, 1),
814 )?;
815 let cancelled = Event::TimerCancelled {
816 cause: TimerCancelCause::WorkflowIntent,
817 envelope: EventEnvelope {
818 seq: 2,
819 recorded_at: instant(69),
820 workflow_id: workflow_id.clone(),
821 },
822 timer_id: timer_id.clone(),
823 };
824 engine.record_workflow_event(&workflow_id, cancelled)?;
825
826 service
827 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
828 .await?;
829
830 let history = history(&store, &workflow_id).await?;
831 assert_eq!(count_timer_fired(&history, &timer_id), 0);
832 assert!(engine.delivered_messages()?.is_empty());
833 Ok(())
834 }
835
836 #[tokio::test]
837 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
838 let process = WorkflowProcessHandle::new(42);
839 let (store, engine, service) = service();
840 let workflow_id = workflow_id();
841 let timer_id = timer_id();
842 let fire_at = instant(80);
843
844 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
845 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
846 engine.record_workflow_event(
847 &workflow_id,
848 timer_started_event(&workflow_id, &timer_id, 1),
849 )?;
850 service
851 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
852 .await?;
853
854 assert_eq!(
855 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
856 1
857 );
858 assert!(engine.delivered_messages()?.is_empty());
859 Ok(())
860 }
861
862 #[tokio::test]
863 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
864 let process = WorkflowProcessHandle::new(42);
865 let (store, engine, service) = service();
866 let workflow_id = workflow_id();
867 let timer_id = timer_id();
868 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
869
870 service
871 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
872 .await?;
873
874 assert!(history(&store, &workflow_id).await?.is_empty());
875 assert!(engine.delivered_messages()?.is_empty());
876 Ok(())
877 }
878
879 struct RecordingDeadlineHandler {
881 calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
882 fail: bool,
883 }
884
885 impl RecordingDeadlineHandler {
886 fn new(fail: bool) -> Self {
887 Self {
888 calls: std::sync::Mutex::new(Vec::new()),
889 fail,
890 }
891 }
892
893 fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
894 self.calls
895 .lock()
896 .map(|calls| calls.clone())
897 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
898 }
899 }
900
901 #[async_trait::async_trait]
902 impl DeadlineHandler for RecordingDeadlineHandler {
903 async fn on_deadline_elapsed(
904 &self,
905 workflow_id: WorkflowId,
906 run_id: RunId,
907 ) -> Result<(), DeadlineHandlerError> {
908 self.calls
909 .lock()
910 .map_err(|error| DeadlineHandlerError(error.to_string()))?
911 .push((workflow_id, run_id));
912 if self.fail {
913 Err(DeadlineHandlerError(
914 "deliberate handler failure".to_owned(),
915 ))
916 } else {
917 Ok(())
918 }
919 }
920 }
921
922 fn service_with_handler(
923 handler: Arc<dyn DeadlineHandler>,
924 ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
925 let concrete_store = Arc::new(InMemoryStore::default());
926 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
927 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
928 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
929 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
930 .with_deadline_handler(handler);
931 (concrete_store, engine, service)
932 }
933
934 #[tokio::test]
937 async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
938 -> Result<(), TimerServiceError> {
939 let run_id = RunId::new_v4();
940 let deadline_id = deadline_timer_id(&run_id)
941 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
942 let handler = Arc::new(RecordingDeadlineHandler::new(false));
943 let (store, engine, service) = service_with_handler(handler.clone());
944 let workflow_id = workflow_id();
945 let fire_at = instant(120);
946 engine.set_residency(
947 workflow_id.clone(),
948 WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
949 )?;
950 engine.record_workflow_event(
951 &workflow_id,
952 timer_started_event(&workflow_id, &deadline_id, 1),
953 )?;
954
955 service
956 .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
957 .await?;
958
959 assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
960 assert_eq!(
961 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
962 0,
963 "a deadline fire never records TimerFired"
964 );
965 assert!(engine.delivered_messages()?.is_empty());
966 Ok(())
967 }
968
969 #[tokio::test]
972 async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
973 let run_id = RunId::new_v4();
974 let deadline_id = deadline_timer_id(&run_id)
975 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
976 let (store, engine, service) = service();
977 let workflow_id = workflow_id();
978 engine.record_workflow_event(
979 &workflow_id,
980 timer_started_event(&workflow_id, &deadline_id, 1),
981 )?;
982
983 let result = service
984 .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
985 .await;
986
987 assert!(
988 matches!(result, Err(TimerServiceError::Deadline(_))),
989 "unhandled deadline fire must be a typed error, got {result:?}"
990 );
991 assert_eq!(
992 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
993 0
994 );
995 Ok(())
996 }
997
998 #[tokio::test]
1000 async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1001 let run_id = RunId::new_v4();
1002 let deadline_id = deadline_timer_id(&run_id)
1003 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1004 let handler = Arc::new(RecordingDeadlineHandler::new(true));
1005 let (_store, engine, service) = service_with_handler(handler);
1006 let workflow_id = workflow_id();
1007 engine.record_workflow_event(
1008 &workflow_id,
1009 timer_started_event(&workflow_id, &deadline_id, 1),
1010 )?;
1011
1012 let result = service
1013 .fire_timer(workflow_id, deadline_id, instant(120))
1014 .await;
1015
1016 assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1017 Ok(())
1018 }
1019
1020 #[tokio::test]
1026 async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1027 -> Result<(), TimerServiceError> {
1028 let process = WorkflowProcessHandle::new(42);
1029 let (store, engine, service) = service();
1030 let workflow_id = workflow_id();
1031 let timer_id = timer_id();
1032 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1033 engine.record_workflow_event(
1034 &workflow_id,
1035 timer_started_event(&workflow_id, &timer_id, 1),
1036 )?;
1037 engine.refuse_next_record_as_terminal()?;
1038
1039 service
1040 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1041 .await?;
1042
1043 assert_eq!(
1044 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1045 0,
1046 "a refused fire records no TimerFired"
1047 );
1048 assert!(
1049 engine.delivered_messages()?.is_empty(),
1050 "a refused fire delivers no wake"
1051 );
1052 Ok(())
1053 }
1054
1055 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1063 async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1064 -> Result<(), TimerServiceError> {
1065 use dashmap::DashSet;
1066 use tokio::sync::Barrier;
1067
1068 for _ in 0..20 {
1069 let process = WorkflowProcessHandle::new(42);
1070 let concrete_store = Arc::new(InMemoryStore::default());
1071 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1072 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1073 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1074 let coordinator = Arc::new(DashSet::new());
1075 let service_a =
1076 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1077 .with_terminal_updates(Arc::clone(&coordinator));
1078 let service_b =
1079 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1080 .with_terminal_updates(Arc::clone(&coordinator));
1081
1082 let workflow_id = workflow_id();
1083 let timer_id = timer_id();
1084 let fire_at = instant(200);
1085 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1086 engine.record_workflow_event(
1087 &workflow_id,
1088 timer_started_event(&workflow_id, &timer_id, 1),
1089 )?;
1090
1091 let gate = Arc::new(Barrier::new(2));
1092 let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1093 let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1094 let cancel = async move {
1095 cancel_gate.wait().await;
1096 service_a
1097 .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1098 .await
1099 };
1100 let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1101 let fire = async move {
1102 fire_gate.wait().await;
1103 service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1104 };
1105 let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1106 cancel_result?;
1107 fire_result?;
1108
1109 let history = history(&concrete_store, &workflow_id).await?;
1110 let terminal_timer_events = history
1111 .iter()
1112 .filter(|event| {
1113 matches!(
1114 event,
1115 Event::TimerFired { timer_id: recorded, .. }
1116 | Event::TimerCancelled { timer_id: recorded, .. }
1117 if recorded == &timer_id
1118 )
1119 })
1120 .count();
1121 assert_eq!(
1122 terminal_timer_events, 1,
1123 "first-recorded wins across shared services: {history:#?}"
1124 );
1125 }
1126 Ok(())
1127 }
1128
1129 #[tokio::test]
1130 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1131 {
1132 let process = WorkflowProcessHandle::new(42);
1133 let (store, engine, service) = service();
1134 let workflow_id = workflow_id();
1135 let timer_id = timer_id();
1136 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1137 engine.record_workflow_event(
1139 &workflow_id,
1140 timer_started_event(&workflow_id, &timer_id, 1),
1141 )?;
1142 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1143
1144 service
1145 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1146 .await?;
1147
1148 assert_eq!(
1149 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1150 0
1151 );
1152 assert!(engine.delivered_messages()?.is_empty());
1153 Ok(())
1154 }
1155}