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(
224 &self,
225 workflow_id: WorkflowId,
226 timer_id: TimerId,
227 fire_at: DateTime<Utc>,
228 ) -> Result<(), TimerServiceError> {
229 let key = (workflow_id.clone(), timer_id.clone());
230 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
231
232 let result = self
233 .fire_timer_guarded(workflow_id, timer_id, fire_at)
234 .await;
235 drop(terminal_update_slot);
236 result
237 }
238
239 async fn wait_for_terminal_update_slot(
240 &self,
241 key: (WorkflowId, TimerId),
242 ) -> TerminalUpdateSlot<'_> {
243 loop {
244 if self.terminal_updates.insert(key.clone()) {
245 return TerminalUpdateSlot {
246 terminal_updates: self.terminal_updates.as_ref(),
247 key,
248 };
249 }
250 tokio::task::yield_now().await;
251 }
252 }
253
254 async fn fire_timer_guarded(
255 &self,
256 workflow_id: WorkflowId,
257 timer_id: TimerId,
258 fire_at: DateTime<Utc>,
259 ) -> Result<(), TimerServiceError> {
260 let history = self.store.read_history(&workflow_id).await?;
270 match timer_disposition_in_active_segment(&history, &timer_id) {
271 TimerDisposition::Live => {}
272 TimerDisposition::Fired if !is_deadline_timer(&timer_id) => {
273 return self
274 .redeliver_recorded_fire(workflow_id, timer_id, fire_at)
275 .await;
276 }
277 TimerDisposition::Fired | TimerDisposition::Cancelled | TimerDisposition::Absent => {
281 return Ok(());
282 }
283 }
284
285 if is_deadline_timer(&timer_id) {
290 return self.fire_deadline(workflow_id, timer_id).await;
291 }
292
293 let event = Event::TimerFired {
294 envelope: self.next_envelope(&workflow_id).await?,
295 timer_id: timer_id.clone(),
296 };
297 match self.engine.record_workflow_event(&workflow_id, event)? {
305 RecordOutcome::RefusedTerminal => return Ok(()),
306 RecordOutcome::Recorded | RecordOutcome::AlreadyRecorded => {}
307 }
308
309 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
310 self.engine.deliver_workflow_message(
311 process,
312 WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
313 )?;
314 }
315
316 Ok(())
317 }
318
319 async fn redeliver_recorded_fire(
339 &self,
340 workflow_id: WorkflowId,
341 timer_id: TimerId,
342 fire_at: DateTime<Utc>,
343 ) -> Result<(), TimerServiceError> {
344 let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)?
345 else {
346 return Ok(());
347 };
348
349 let event = Event::TimerFired {
350 envelope: self.next_envelope(&workflow_id).await?,
351 timer_id: timer_id.clone(),
352 };
353 match self.engine.record_workflow_event(&workflow_id, event)? {
354 RecordOutcome::RefusedTerminal => return Ok(()),
357 RecordOutcome::Recorded | RecordOutcome::AlreadyRecorded => {}
358 }
359
360 self.engine.deliver_workflow_message(
361 process,
362 WorkflowMailboxMessage::TimerFired {
363 timer_id: timer_id.clone(),
364 fire_at,
365 },
366 )?;
367 tracing::info!(
368 %workflow_id,
369 %timer_id,
370 "timer fire was already durably recorded; delivered the owed mailbox wake"
371 );
372 Ok(())
373 }
374
375 async fn fire_deadline(
384 &self,
385 workflow_id: WorkflowId,
386 timer_id: TimerId,
387 ) -> Result<(), TimerServiceError> {
388 let handler = self.deadline_handler.as_ref().ok_or_else(|| {
389 TimerServiceError::Deadline(format!(
390 "no deadline handler registered for {timer_id} on workflow {workflow_id}"
391 ))
392 })?;
393 let run_id = deadline_run_id(&timer_id).ok_or_else(|| {
394 TimerServiceError::Deadline(format!(
395 "malformed deadline timer {timer_id} on workflow {workflow_id}"
396 ))
397 })?;
398 handler
399 .on_deadline_elapsed(workflow_id, run_id)
400 .await
401 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
402 }
403
404 async fn timer_is_live(
420 &self,
421 workflow_id: &WorkflowId,
422 timer_id: &TimerId,
423 ) -> Result<bool, StoreError> {
424 let history = self.store.read_history(workflow_id).await?;
425 Ok(matches!(
426 timer_disposition_in_active_segment(&history, timer_id),
427 TimerDisposition::Live
428 ))
429 }
430
431 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
432 let history = self.store.read_history(workflow_id).await?;
433 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
434 Ok(EventEnvelope {
435 seq,
436 recorded_at: (self.recorded_at)(),
437 workflow_id: workflow_id.clone(),
438 })
439 }
440}
441
442pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
457 let mut live: Vec<TimerId> = Vec::new();
458 for event in active_segment(history) {
459 match event {
460 Event::TimerStarted { timer_id, .. } if !live.contains(timer_id) => {
461 live.push(timer_id.clone());
462 }
463 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
464 live.retain(|id| id != timer_id);
465 }
466 _ => {}
467 }
468 }
469 live
470}
471
472fn active_segment(history: &[Event]) -> &[Event] {
481 let segment_start = history
482 .iter()
483 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
484 .unwrap_or(0);
485 &history[segment_start..]
486}
487
488#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub(crate) enum TimerDisposition {
501 Live,
503 Fired,
508 Cancelled,
510 Absent,
513}
514
515pub(crate) fn timer_disposition_in_active_segment(
517 history: &[Event],
518 timer_id: &TimerId,
519) -> TimerDisposition {
520 let mut disposition = TimerDisposition::Absent;
521 for event in active_segment(history) {
522 match event {
523 Event::TimerStarted { timer_id: id, .. } if id == timer_id => {
524 disposition = TimerDisposition::Live;
525 }
526 Event::TimerFired { timer_id: id, .. } if id == timer_id => {
527 disposition = TimerDisposition::Fired;
528 }
529 Event::TimerCancelled { timer_id: id, .. } if id == timer_id => {
530 disposition = TimerDisposition::Cancelled;
531 }
532 _ => {}
533 }
534 }
535 disposition
536}
537
538#[cfg(test)]
539mod tests {
540 use std::sync::Arc;
541
542 use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
543 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
544 use chrono::{DateTime, Utc};
545
546 use super::{
547 TimerDisposition, TimerService, TimerServiceError, live_timers_in_active_segment,
548 timer_disposition_in_active_segment,
549 };
550 use crate::engine_seam::test_support::{
551 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
552 };
553 use crate::engine_seam::{
554 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
555 };
556 use crate::time::deadline::{DeadlineHandler, DeadlineHandlerError, deadline_timer_id};
557
558 fn instant(offset_seconds: i64) -> DateTime<Utc> {
559 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
560 }
561
562 fn workflow_id() -> WorkflowId {
563 WorkflowId::new_v4()
564 }
565
566 fn timer_id() -> TimerId {
567 TimerId::anonymous(7)
568 }
569
570 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
571 let concrete_store = Arc::new(InMemoryStore::default());
572 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
573 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
574 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
575 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
576 (concrete_store, engine, service)
577 }
578
579 fn recorded_at() -> DateTime<Utc> {
580 instant(1)
581 }
582
583 async fn history(
584 store: &InMemoryStore,
585 workflow_id: &WorkflowId,
586 ) -> Result<Vec<Event>, StoreError> {
587 store.read_history(workflow_id).await
588 }
589
590 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
591 events
592 .iter()
593 .filter(|event| {
594 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
595 })
596 .count()
597 }
598
599 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
600 Event::TimerStarted {
601 envelope: EventEnvelope {
602 seq,
603 recorded_at: instant(0),
604 workflow_id: workflow_id.clone(),
605 },
606 timer_id: timer_id.clone(),
607 fire_at: instant(5),
608 }
609 }
610
611 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
612 Event::WorkflowStarted {
613 envelope: EventEnvelope {
614 seq,
615 recorded_at: instant(0),
616 workflow_id: workflow_id.clone(),
617 },
618 workflow_type: "fixture".to_owned(),
619 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
620 run_id: aion_core::RunId::new_v4(),
621 parent_run_id: None,
622 parent_workflow_id: None,
623 package_version: aion_core::PackageVersion::new("a".repeat(64)),
624 }
625 }
626
627 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
628 Event::TimerFired {
629 envelope: EventEnvelope {
630 seq,
631 recorded_at: instant(0),
632 workflow_id: workflow_id.clone(),
633 },
634 timer_id: timer_id.clone(),
635 }
636 }
637
638 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
639 Event::TimerCancelled {
640 cause: TimerCancelCause::WorkflowIntent,
641 envelope: EventEnvelope {
642 seq,
643 recorded_at: instant(0),
644 workflow_id: workflow_id.clone(),
645 },
646 timer_id: timer_id.clone(),
647 }
648 }
649
650 fn make_named(name: &str) -> TimerId {
651 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
655 }
656
657 fn named_timer_id() -> TimerId {
658 make_named("review-deadline")
659 }
660
661 #[test]
664 fn started_timer_is_live() {
665 let workflow_id = workflow_id();
666 let timer_id = named_timer_id();
667 let history = vec![
668 workflow_started_event(&workflow_id, 0),
669 timer_started_event(&workflow_id, &timer_id, 1),
670 ];
671 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
672 }
673
674 #[test]
675 fn started_then_fired_timer_is_dead() {
676 let workflow_id = workflow_id();
677 let timer_id = named_timer_id();
678 let history = vec![
679 workflow_started_event(&workflow_id, 0),
680 timer_started_event(&workflow_id, &timer_id, 1),
681 timer_fired_event(&workflow_id, &timer_id, 2),
682 ];
683 assert!(live_timers_in_active_segment(&history).is_empty());
684 }
685
686 #[test]
687 fn started_then_cancelled_timer_is_dead() {
688 let workflow_id = workflow_id();
689 let timer_id = named_timer_id();
690 let history = vec![
691 workflow_started_event(&workflow_id, 0),
692 timer_started_event(&workflow_id, &timer_id, 1),
693 timer_cancelled_event(&workflow_id, &timer_id, 2),
694 ];
695 assert!(live_timers_in_active_segment(&history).is_empty());
696 }
697
698 #[test]
699 fn restarted_named_timer_after_fire_is_live() {
700 let workflow_id = workflow_id();
704 let timer_id = named_timer_id();
705 let history = vec![
706 workflow_started_event(&workflow_id, 0),
707 timer_started_event(&workflow_id, &timer_id, 1),
708 timer_fired_event(&workflow_id, &timer_id, 2),
709 timer_started_event(&workflow_id, &timer_id, 3),
710 ];
711 assert_eq!(
712 live_timers_in_active_segment(&history),
713 vec![timer_id],
714 "a re-armed named timer is live again"
715 );
716 }
717
718 #[test]
719 fn restarted_named_timer_after_cancel_is_live() {
720 let workflow_id = workflow_id();
721 let timer_id = named_timer_id();
722 let history = vec![
723 workflow_started_event(&workflow_id, 0),
724 timer_started_event(&workflow_id, &timer_id, 1),
725 timer_cancelled_event(&workflow_id, &timer_id, 2),
726 timer_started_event(&workflow_id, &timer_id, 3),
727 ];
728 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
729 }
730
731 #[test]
732 fn prior_run_segment_timer_is_not_live() {
733 let workflow_id = workflow_id();
736 let prior = named_timer_id();
737 let current = make_named("current-deadline");
738 let history = vec![
739 workflow_started_event(&workflow_id, 0),
740 timer_started_event(&workflow_id, &prior, 1),
741 workflow_started_event(&workflow_id, 2),
743 timer_started_event(&workflow_id, ¤t, 3),
744 ];
745 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
746 }
747
748 #[test]
753 fn disposition_tracks_the_last_event_for_the_id_and_agrees_with_liveness() {
754 let workflow_id = workflow_id();
755 let timer_id = named_timer_id();
756 let other = make_named("unrelated");
757 let assert_agrees = |history: &[Event], expected: TimerDisposition| {
758 assert_eq!(
759 timer_disposition_in_active_segment(history, &timer_id),
760 expected
761 );
762 assert_eq!(
763 live_timers_in_active_segment(history).contains(&timer_id),
764 expected == TimerDisposition::Live,
765 "the per-id disposition and the enumerating model disagree on liveness"
766 );
767 };
768
769 let mut history = vec![
771 workflow_started_event(&workflow_id, 0),
772 timer_started_event(&workflow_id, &timer_id, 1),
773 ];
774 assert_agrees(&history, TimerDisposition::Live);
775
776 history.push(timer_fired_event(&workflow_id, &timer_id, 2));
778 assert_agrees(&history, TimerDisposition::Fired);
779
780 history.push(timer_fired_event(&workflow_id, &other, 3));
782 assert_agrees(&history, TimerDisposition::Fired);
783
784 history.push(timer_started_event(&workflow_id, &timer_id, 4));
786 assert_agrees(&history, TimerDisposition::Live);
787
788 history.push(timer_cancelled_event(&workflow_id, &timer_id, 5));
790 assert_agrees(&history, TimerDisposition::Cancelled);
791
792 history.push(workflow_started_event(&workflow_id, 6));
795 assert_agrees(&history, TimerDisposition::Absent);
796
797 assert_agrees(&[], TimerDisposition::Absent);
799 }
800
801 #[tokio::test]
802 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
803 let process = WorkflowProcessHandle::new(42);
808 let (store, engine, service) = service();
809 let workflow_id = workflow_id();
810 let timer_id = named_timer_id();
811 let fire_at = instant(110);
812 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
813 engine.record_workflow_event(
814 &workflow_id,
815 timer_started_event(&workflow_id, &timer_id, 1),
816 )?;
817 engine
818 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
819 engine.record_workflow_event(
820 &workflow_id,
821 timer_started_event(&workflow_id, &timer_id, 3),
822 )?;
823
824 service
825 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
826 .await?;
827
828 assert_eq!(
829 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
830 2,
831 "the re-armed timer fires again, recording a second TimerFired"
832 );
833 assert_eq!(engine.delivered_messages()?.len(), 1);
834 Ok(())
835 }
836
837 #[tokio::test]
838 async fn schedule_records_timer_row_without_timer_started_event()
839 -> Result<(), TimerServiceError> {
840 let (store, _engine, service) = service();
841 let workflow_id = workflow_id();
842 let timer_id = timer_id();
843 let fire_at = instant(10);
844
845 service
846 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
847 .await?;
848
849 let expired = store.expired_timers(fire_at).await?;
850 assert_eq!(expired.len(), 1);
851 assert_eq!(expired[0].workflow_id, workflow_id);
852 assert_eq!(expired[0].timer_id, timer_id);
853 assert_eq!(expired[0].fire_at, fire_at);
854
855 assert!(history(&store, &workflow_id).await?.is_empty());
856 Ok(())
857 }
858
859 #[tokio::test]
860 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
861 let process = WorkflowProcessHandle::new(42);
862 let (_store, engine, service) = service();
863 let workflow_id = workflow_id();
864 let timer_id = timer_id();
865 let fire_at = instant(20);
866 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
867
868 service
869 .schedule(workflow_id, timer_id.clone(), fire_at)
870 .await?;
871
872 assert_eq!(
873 engine.armed_timers()?,
874 vec![TimerWheelEntry {
875 process,
876 timer_id,
877 fire_at
878 }]
879 );
880 Ok(())
881 }
882
883 #[tokio::test]
884 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
885 let (store, engine, service) = service();
886 let workflow_id = workflow_id();
887 let timer_id = timer_id();
888 let fire_at = instant(30);
889 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
890
891 service
892 .schedule(workflow_id.clone(), timer_id, fire_at)
893 .await?;
894
895 assert!(engine.armed_timers()?.is_empty());
896 assert!(history(&store, &workflow_id).await?.is_empty());
897 Ok(())
898 }
899
900 #[tokio::test]
901 async fn fire_records_timer_fired_then_delivers_mailbox_message()
902 -> Result<(), TimerServiceError> {
903 let process = WorkflowProcessHandle::new(42);
904 let (store, engine, service) = service();
905 let workflow_id = workflow_id();
906 let timer_id = timer_id();
907 let fire_at = instant(40);
908 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
909 engine.record_workflow_event(
910 &workflow_id,
911 timer_started_event(&workflow_id, &timer_id, 1),
912 )?;
913
914 service
915 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
916 .await?;
917
918 assert_eq!(
919 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
920 1
921 );
922 assert_eq!(
923 engine.delivered_messages()?,
924 vec![(
925 process,
926 DeliveredWorkflowMessage::TimerFired {
927 timer_id: timer_id.clone(),
928 fire_at
929 }
930 )]
931 );
932 assert!(matches!(
933 engine.operations()?.as_slice(),
934 [
935 FakeEngineOperation::EventRecorded {
936 event: Event::TimerStarted { .. },
937 ..
938 },
939 FakeEngineOperation::EventRecorded {
940 workflow_id: recorded_workflow_id,
941 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
942 },
943 FakeEngineOperation::Delivered {
944 process: delivered_process,
945 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
946 }
947 ] if recorded_workflow_id == &workflow_id
948 && recorded_timer_id == &timer_id
949 && delivered_process == &process
950 && delivered_timer_id == &timer_id
951 ));
952 Ok(())
953 }
954
955 #[tokio::test]
956 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
957 -> Result<(), TimerServiceError> {
958 let (store, engine, service) = service();
959 let workflow_id = workflow_id();
960 let timer_id = timer_id();
961 let fire_at = instant(50);
962 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
963 engine.record_workflow_event(
964 &workflow_id,
965 timer_started_event(&workflow_id, &timer_id, 1),
966 )?;
967
968 service
969 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
970 .await?;
971
972 assert_eq!(
973 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
974 1
975 );
976 assert!(engine.delivered_messages()?.is_empty());
977 Ok(())
978 }
979
980 #[tokio::test]
989 async fn firing_same_timer_twice_records_once_and_redelivers_the_wake()
990 -> Result<(), TimerServiceError> {
991 let process = WorkflowProcessHandle::new(42);
992 let (store, engine, service) = service();
993 let workflow_id = workflow_id();
994 let timer_id = timer_id();
995 let fire_at = instant(60);
996 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
997 engine.record_workflow_event(
998 &workflow_id,
999 timer_started_event(&workflow_id, &timer_id, 1),
1000 )?;
1001
1002 service
1003 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1004 .await?;
1005 service
1011 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1012 .await?;
1013
1014 assert_eq!(
1015 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1016 1,
1017 "the recorded fire must never be appended a second time"
1018 );
1019 assert_eq!(
1020 engine.delivered_messages()?.len(),
1021 2,
1022 "the second fire re-delivers the owed wake instead of silently no-opping"
1023 );
1024 Ok(())
1025 }
1026
1027 #[tokio::test]
1035 async fn already_recorded_fire_delivers_owed_wake_without_second_append()
1036 -> Result<(), TimerServiceError> {
1037 let process = WorkflowProcessHandle::new(42);
1038 let (store, engine, service) = service();
1039 let workflow_id = workflow_id();
1040 let timer_id = timer_id();
1041 let fire_at = instant(140);
1042 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1043 engine.record_workflow_event(
1044 &workflow_id,
1045 timer_started_event(&workflow_id, &timer_id, 1),
1046 )?;
1047 engine
1048 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1049
1050 service
1051 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1052 .await?;
1053
1054 assert_eq!(
1055 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1056 1,
1057 "the already-recorded fire must not be appended again"
1058 );
1059 assert_eq!(
1060 engine.delivered_messages()?,
1061 vec![(
1062 process,
1063 DeliveredWorkflowMessage::TimerFired { timer_id, fire_at }
1064 )],
1065 "the owed mailbox wake must be delivered"
1066 );
1067 Ok(())
1068 }
1069
1070 #[tokio::test]
1077 async fn already_recorded_fire_for_nonresident_workflow_wakes_nothing()
1078 -> Result<(), TimerServiceError> {
1079 let (store, engine, service) = service();
1080 let workflow_id = workflow_id();
1081 let timer_id = timer_id();
1082 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1083 engine.record_workflow_event(
1084 &workflow_id,
1085 timer_started_event(&workflow_id, &timer_id, 1),
1086 )?;
1087 engine
1088 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1089
1090 service
1091 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(150))
1092 .await?;
1093
1094 assert_eq!(
1095 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1096 1,
1097 "a non-resident redelivery must not re-enter the recorder seam"
1098 );
1099 assert!(
1100 engine.delivered_messages()?.is_empty(),
1101 "no wake is attempted for a non-resident workflow"
1102 );
1103 Ok(())
1104 }
1105
1106 #[tokio::test]
1112 async fn already_recorded_fire_after_run_terminal_delivers_no_wake()
1113 -> Result<(), TimerServiceError> {
1114 let process = WorkflowProcessHandle::new(42);
1115 let (store, engine, service) = service();
1116 let workflow_id = workflow_id();
1117 let timer_id = timer_id();
1118 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1119 engine.record_workflow_event(
1120 &workflow_id,
1121 timer_started_event(&workflow_id, &timer_id, 1),
1122 )?;
1123 engine
1124 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1125 engine.refuse_next_record_as_terminal()?;
1126
1127 service
1128 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(160))
1129 .await?;
1130
1131 assert_eq!(
1132 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1133 1
1134 );
1135 assert!(
1136 engine.delivered_messages()?.is_empty(),
1137 "a post-terminal redelivery must not wake the terminated run"
1138 );
1139 Ok(())
1140 }
1141
1142 #[tokio::test]
1143 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
1144 let process = WorkflowProcessHandle::new(42);
1145 let (store, engine, service) = service();
1146 let workflow_id = workflow_id();
1147 let timer_id = timer_id();
1148 let fire_at = instant(70);
1149 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1150 engine.record_workflow_event(
1151 &workflow_id,
1152 timer_started_event(&workflow_id, &timer_id, 1),
1153 )?;
1154 let cancelled = Event::TimerCancelled {
1155 cause: TimerCancelCause::WorkflowIntent,
1156 envelope: EventEnvelope {
1157 seq: 2,
1158 recorded_at: instant(69),
1159 workflow_id: workflow_id.clone(),
1160 },
1161 timer_id: timer_id.clone(),
1162 };
1163 engine.record_workflow_event(&workflow_id, cancelled)?;
1164
1165 service
1166 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1167 .await?;
1168
1169 let history = history(&store, &workflow_id).await?;
1170 assert_eq!(count_timer_fired(&history, &timer_id), 0);
1171 assert!(engine.delivered_messages()?.is_empty());
1172 Ok(())
1173 }
1174
1175 #[tokio::test]
1176 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
1177 let process = WorkflowProcessHandle::new(42);
1178 let (store, engine, service) = service();
1179 let workflow_id = workflow_id();
1180 let timer_id = timer_id();
1181 let fire_at = instant(80);
1182
1183 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1184 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1185 engine.record_workflow_event(
1186 &workflow_id,
1187 timer_started_event(&workflow_id, &timer_id, 1),
1188 )?;
1189 service
1190 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1191 .await?;
1192
1193 assert_eq!(
1194 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1195 1
1196 );
1197 assert!(engine.delivered_messages()?.is_empty());
1198 Ok(())
1199 }
1200
1201 #[tokio::test]
1202 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
1203 let process = WorkflowProcessHandle::new(42);
1204 let (store, engine, service) = service();
1205 let workflow_id = workflow_id();
1206 let timer_id = timer_id();
1207 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1208
1209 service
1210 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
1211 .await?;
1212
1213 assert!(history(&store, &workflow_id).await?.is_empty());
1214 assert!(engine.delivered_messages()?.is_empty());
1215 Ok(())
1216 }
1217
1218 struct RecordingDeadlineHandler {
1220 calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
1221 fail: bool,
1222 }
1223
1224 impl RecordingDeadlineHandler {
1225 fn new(fail: bool) -> Self {
1226 Self {
1227 calls: std::sync::Mutex::new(Vec::new()),
1228 fail,
1229 }
1230 }
1231
1232 fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
1233 self.calls
1234 .lock()
1235 .map(|calls| calls.clone())
1236 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
1237 }
1238 }
1239
1240 #[async_trait::async_trait]
1241 impl DeadlineHandler for RecordingDeadlineHandler {
1242 async fn on_deadline_elapsed(
1243 &self,
1244 workflow_id: WorkflowId,
1245 run_id: RunId,
1246 ) -> Result<(), DeadlineHandlerError> {
1247 self.calls
1248 .lock()
1249 .map_err(|error| DeadlineHandlerError(error.to_string()))?
1250 .push((workflow_id, run_id));
1251 if self.fail {
1252 Err(DeadlineHandlerError(
1253 "deliberate handler failure".to_owned(),
1254 ))
1255 } else {
1256 Ok(())
1257 }
1258 }
1259 }
1260
1261 fn service_with_handler(
1262 handler: Arc<dyn DeadlineHandler>,
1263 ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
1264 let concrete_store = Arc::new(InMemoryStore::default());
1265 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1266 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
1267 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1268 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
1269 .with_deadline_handler(handler);
1270 (concrete_store, engine, service)
1271 }
1272
1273 #[tokio::test]
1276 async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
1277 -> Result<(), TimerServiceError> {
1278 let run_id = RunId::new_v4();
1279 let deadline_id = deadline_timer_id(&run_id)
1280 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1281 let handler = Arc::new(RecordingDeadlineHandler::new(false));
1282 let (store, engine, service) = service_with_handler(handler.clone());
1283 let workflow_id = workflow_id();
1284 let fire_at = instant(120);
1285 engine.set_residency(
1286 workflow_id.clone(),
1287 WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
1288 )?;
1289 engine.record_workflow_event(
1290 &workflow_id,
1291 timer_started_event(&workflow_id, &deadline_id, 1),
1292 )?;
1293
1294 service
1295 .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
1296 .await?;
1297
1298 assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
1299 assert_eq!(
1300 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1301 0,
1302 "a deadline fire never records TimerFired"
1303 );
1304 assert!(engine.delivered_messages()?.is_empty());
1305 Ok(())
1306 }
1307
1308 #[tokio::test]
1311 async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
1312 let run_id = RunId::new_v4();
1313 let deadline_id = deadline_timer_id(&run_id)
1314 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1315 let (store, engine, service) = service();
1316 let workflow_id = workflow_id();
1317 engine.record_workflow_event(
1318 &workflow_id,
1319 timer_started_event(&workflow_id, &deadline_id, 1),
1320 )?;
1321
1322 let result = service
1323 .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
1324 .await;
1325
1326 assert!(
1327 matches!(result, Err(TimerServiceError::Deadline(_))),
1328 "unhandled deadline fire must be a typed error, got {result:?}"
1329 );
1330 assert_eq!(
1331 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1332 0
1333 );
1334 Ok(())
1335 }
1336
1337 #[tokio::test]
1339 async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1340 let run_id = RunId::new_v4();
1341 let deadline_id = deadline_timer_id(&run_id)
1342 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1343 let handler = Arc::new(RecordingDeadlineHandler::new(true));
1344 let (_store, engine, service) = service_with_handler(handler);
1345 let workflow_id = workflow_id();
1346 engine.record_workflow_event(
1347 &workflow_id,
1348 timer_started_event(&workflow_id, &deadline_id, 1),
1349 )?;
1350
1351 let result = service
1352 .fire_timer(workflow_id, deadline_id, instant(120))
1353 .await;
1354
1355 assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1356 Ok(())
1357 }
1358
1359 #[tokio::test]
1365 async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1366 -> Result<(), TimerServiceError> {
1367 let process = WorkflowProcessHandle::new(42);
1368 let (store, engine, service) = service();
1369 let workflow_id = workflow_id();
1370 let timer_id = timer_id();
1371 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1372 engine.record_workflow_event(
1373 &workflow_id,
1374 timer_started_event(&workflow_id, &timer_id, 1),
1375 )?;
1376 engine.refuse_next_record_as_terminal()?;
1377
1378 service
1379 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1380 .await?;
1381
1382 assert_eq!(
1383 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1384 0,
1385 "a refused fire records no TimerFired"
1386 );
1387 assert!(
1388 engine.delivered_messages()?.is_empty(),
1389 "a refused fire delivers no wake"
1390 );
1391 Ok(())
1392 }
1393
1394 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1402 async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1403 -> Result<(), TimerServiceError> {
1404 use dashmap::DashSet;
1405 use tokio::sync::Barrier;
1406
1407 for _ in 0..20 {
1408 let process = WorkflowProcessHandle::new(42);
1409 let concrete_store = Arc::new(InMemoryStore::default());
1410 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1411 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1412 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1413 let coordinator = Arc::new(DashSet::new());
1414 let service_a =
1415 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1416 .with_terminal_updates(Arc::clone(&coordinator));
1417 let service_b =
1418 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1419 .with_terminal_updates(Arc::clone(&coordinator));
1420
1421 let workflow_id = workflow_id();
1422 let timer_id = timer_id();
1423 let fire_at = instant(200);
1424 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1425 engine.record_workflow_event(
1426 &workflow_id,
1427 timer_started_event(&workflow_id, &timer_id, 1),
1428 )?;
1429
1430 let gate = Arc::new(Barrier::new(2));
1431 let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1432 let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1433 let cancel = async move {
1434 cancel_gate.wait().await;
1435 service_a
1436 .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1437 .await
1438 };
1439 let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1440 let fire = async move {
1441 fire_gate.wait().await;
1442 service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1443 };
1444 let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1445 cancel_result?;
1446 fire_result?;
1447
1448 let history = history(&concrete_store, &workflow_id).await?;
1449 let terminal_timer_events = history
1450 .iter()
1451 .filter(|event| {
1452 matches!(
1453 event,
1454 Event::TimerFired { timer_id: recorded, .. }
1455 | Event::TimerCancelled { timer_id: recorded, .. }
1456 if recorded == &timer_id
1457 )
1458 })
1459 .count();
1460 assert_eq!(
1461 terminal_timer_events, 1,
1462 "first-recorded wins across shared services: {history:#?}"
1463 );
1464 }
1465 Ok(())
1466 }
1467
1468 #[tokio::test]
1469 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1470 {
1471 let process = WorkflowProcessHandle::new(42);
1472 let (store, engine, service) = service();
1473 let workflow_id = workflow_id();
1474 let timer_id = timer_id();
1475 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1476 engine.record_workflow_event(
1478 &workflow_id,
1479 timer_started_event(&workflow_id, &timer_id, 1),
1480 )?;
1481 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1482
1483 service
1484 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1485 .await?;
1486
1487 assert_eq!(
1488 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1489 0
1490 );
1491 assert!(engine.delivered_messages()?.is_empty());
1492 Ok(())
1493 }
1494}