1use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, TimerCancelCause, TimerId, WorkflowId, status_from_events};
6use aion_store::{ReadableEventStore, StoreError, TimerRetirement};
7use chrono::{DateTime, Utc};
8use dashmap::DashSet;
9
10use crate::engine_seam::{
11 EngineHandle, EngineSeamError, RecordOutcome, RedeliveredFire, TimerWheelEntry,
12 WorkflowMailboxMessage, WorkflowResidency,
13};
14use crate::time::deadline::{DeadlineHandler, deadline_run_id, is_deadline_timer};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub(crate) enum RetireAttempt {
24 Retired,
26 Superseded,
28 Failed,
31}
32
33pub struct TimerService {
39 engine: Arc<dyn EngineHandle>,
40 store: Arc<dyn ReadableEventStore>,
41 recorded_at: fn() -> DateTime<Utc>,
42 terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
50 deadline_handler: Option<Arc<dyn DeadlineHandler>>,
56}
57
58struct TerminalUpdateSlot<'a> {
59 terminal_updates: &'a DashSet<(WorkflowId, TimerId)>,
60 key: (WorkflowId, TimerId),
61}
62
63impl Drop for TerminalUpdateSlot<'_> {
64 fn drop(&mut self) {
65 self.terminal_updates.remove(&self.key);
66 }
67}
68
69#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
71pub enum TimerServiceError {
72 #[error("timer store operation failed: {0}")]
74 Store(#[from] StoreError),
75
76 #[error("timer engine operation failed: {0}")]
78 Engine(#[from] EngineSeamError),
79
80 #[error("deadline timer routing failed: {0}")]
88 Deadline(String),
89}
90
91impl TimerService {
92 #[must_use]
94 pub fn new(engine: Arc<dyn EngineHandle>, store: Arc<dyn ReadableEventStore>) -> Self {
95 Self::with_recorded_at(engine, store, Utc::now)
96 }
97
98 #[must_use]
100 pub fn with_recorded_at(
101 engine: Arc<dyn EngineHandle>,
102 store: Arc<dyn ReadableEventStore>,
103 recorded_at: fn() -> DateTime<Utc>,
104 ) -> Self {
105 Self {
106 engine,
107 store,
108 recorded_at,
109 terminal_updates: Arc::new(DashSet::new()),
110 deadline_handler: None,
111 }
112 }
113
114 #[must_use]
122 pub fn with_terminal_updates(
123 mut self,
124 terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
125 ) -> Self {
126 self.terminal_updates = terminal_updates;
127 self
128 }
129
130 #[must_use]
137 pub fn with_deadline_handler(mut self, handler: Arc<dyn DeadlineHandler>) -> Self {
138 self.deadline_handler = Some(handler);
139 self
140 }
141
142 pub async fn schedule(
156 &self,
157 workflow_id: WorkflowId,
158 timer_id: TimerId,
159 fire_at: DateTime<Utc>,
160 armed_seq: u64,
161 ) -> Result<(), TimerServiceError> {
162 self.store
163 .schedule_timer(&workflow_id, &timer_id, fire_at, armed_seq)
164 .await?;
165
166 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
167 self.engine.arm_timer(TimerWheelEntry {
168 process,
169 timer_id,
170 fire_at,
171 })?;
172 }
173
174 Ok(())
175 }
176
177 pub async fn cancel(
194 &self,
195 workflow_id: WorkflowId,
196 timer_id: TimerId,
197 cause: TimerCancelCause,
198 ) -> Result<(), TimerServiceError> {
199 let key = (workflow_id.clone(), timer_id.clone());
200 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
201
202 let result = self.cancel_guarded(workflow_id, timer_id, cause).await;
203 drop(terminal_update_slot);
204 result
205 }
206
207 async fn cancel_guarded(
208 &self,
209 workflow_id: WorkflowId,
210 timer_id: TimerId,
211 cause: TimerCancelCause,
212 ) -> Result<(), TimerServiceError> {
213 let history = self.store.read_history(&workflow_id).await?;
219 if !matches!(
220 timer_disposition_in_active_segment(&history, &timer_id),
221 TimerDisposition::Live
222 ) {
223 return Ok(());
224 }
225 let arming = last_recorded_arming(&history, &timer_id);
226
227 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
228 self.engine.disarm_timer(process, &timer_id)?;
229 }
230
231 let event = Event::TimerCancelled {
232 envelope: self.next_envelope(&workflow_id).await?,
233 timer_id: timer_id.clone(),
234 cause,
235 };
236 self.engine.record_workflow_event(&workflow_id, event)?;
237
238 if let Some((fire_at, armed_seq)) = arming {
242 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
243 .await;
244 }
245
246 Ok(())
247 }
248
249 pub async fn fire_timer(
263 &self,
264 workflow_id: WorkflowId,
265 timer_id: TimerId,
266 fire_at: DateTime<Utc>,
267 ) -> Result<(), TimerServiceError> {
268 let key = (workflow_id.clone(), timer_id.clone());
269 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
270
271 let result = self
272 .fire_timer_guarded(workflow_id, timer_id, fire_at)
273 .await;
274 drop(terminal_update_slot);
275 result
276 }
277
278 async fn wait_for_terminal_update_slot(
279 &self,
280 key: (WorkflowId, TimerId),
281 ) -> TerminalUpdateSlot<'_> {
282 loop {
283 if self.terminal_updates.insert(key.clone()) {
284 return TerminalUpdateSlot {
285 terminal_updates: self.terminal_updates.as_ref(),
286 key,
287 };
288 }
289 tokio::task::yield_now().await;
290 }
291 }
292
293 async fn fire_timer_guarded(
294 &self,
295 workflow_id: WorkflowId,
296 timer_id: TimerId,
297 fire_at: DateTime<Utc>,
298 ) -> Result<(), TimerServiceError> {
299 let history = self.store.read_history(&workflow_id).await?;
309 let armed_seq = last_recorded_arming(&history, &timer_id).map_or(0, |(_, seq)| seq);
315 match timer_disposition_in_active_segment(&history, &timer_id) {
316 TimerDisposition::Live => {}
317 TimerDisposition::Fired if !is_deadline_timer(&timer_id) => {
318 return self
319 .redeliver_owed_wake(workflow_id, timer_id, fire_at, armed_seq)
320 .await
321 .map(|_| ());
322 }
323 TimerDisposition::Fired | TimerDisposition::Cancelled | TimerDisposition::Absent => {
329 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
330 .await;
331 return Ok(());
332 }
333 }
334
335 if is_deadline_timer(&timer_id) {
340 self.fire_deadline(workflow_id.clone(), timer_id.clone())
341 .await?;
342 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
348 .await;
349 return Ok(());
350 }
351
352 let event = Event::TimerFired {
353 envelope: self.next_envelope(&workflow_id).await?,
354 timer_id: timer_id.clone(),
355 };
356 match self.engine.record_workflow_event(&workflow_id, event)? {
364 RecordOutcome::RefusedTerminal | RecordOutcome::RefusedRetired => {
370 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
376 .await;
377 return Ok(());
378 }
379 RecordOutcome::Recorded | RecordOutcome::AlreadyRecorded => {}
380 }
381
382 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)?
383 && let Err(delivery) = self.engine.deliver_workflow_message(
384 process,
385 WorkflowMailboxMessage::TimerFired {
386 timer_id: timer_id.clone(),
387 fire_at,
388 },
389 )
390 {
391 let after_wake = self.store.read_history(&workflow_id).await?;
405 if !status_from_events(&after_wake).is_terminal() {
406 return Err(delivery.into());
407 }
408 tracing::warn!(
409 %workflow_id,
410 timer_id = %timer_id,
411 pid = process.pid(),
412 error = %delivery,
413 "recovered timer fire recorded, but the wake found the run already \
414 terminal and its process gone — nothing owed; the row retires (aion#215)"
415 );
416 }
417
418 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
423 .await;
424
425 Ok(())
426 }
427
428 pub(crate) async fn redeliver_owed_wake(
459 &self,
460 workflow_id: WorkflowId,
461 timer_id: TimerId,
462 fire_at: DateTime<Utc>,
463 armed_seq: u64,
464 ) -> Result<(bool, RetireAttempt), TimerServiceError> {
465 let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)?
466 else {
467 let row = self
472 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
473 .await;
474 return Ok((false, row));
475 };
476
477 match self
478 .engine
479 .record_redelivered_timer_fire(&workflow_id, &timer_id)?
480 {
481 RedeliveredFire::RefusedTerminal | RedeliveredFire::NotOwed => {
487 let row = self
488 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
489 .await;
490 Ok((false, row))
491 }
492 RedeliveredFire::WakeOwed => {
493 self.engine.deliver_workflow_message(
494 process,
495 WorkflowMailboxMessage::TimerFired {
496 timer_id: timer_id.clone(),
497 fire_at,
498 },
499 )?;
500 tracing::info!(
501 %workflow_id,
502 %timer_id,
503 "timer fire was already durably recorded; delivered the owed mailbox wake"
504 );
505 let row = self
506 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
507 .await;
508 Ok((true, row))
509 }
510 }
511 }
512
513 async fn fire_deadline(
522 &self,
523 workflow_id: WorkflowId,
524 timer_id: TimerId,
525 ) -> Result<(), TimerServiceError> {
526 let handler = self.deadline_handler.as_ref().ok_or_else(|| {
527 TimerServiceError::Deadline(format!(
528 "no deadline handler registered for {timer_id} on workflow {workflow_id}"
529 ))
530 })?;
531 let run_id = deadline_run_id(&timer_id).ok_or_else(|| {
532 TimerServiceError::Deadline(format!(
533 "malformed deadline timer {timer_id} on workflow {workflow_id}"
534 ))
535 })?;
536 handler
537 .on_deadline_elapsed(workflow_id, run_id)
538 .await
539 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
540 }
541
542 pub(crate) async fn retire_consumed_row(
561 &self,
562 workflow_id: &WorkflowId,
563 timer_id: &TimerId,
564 fire_at: DateTime<Utc>,
565 armed_seq: u64,
566 ) -> RetireAttempt {
567 match self
568 .store
569 .retire_timer(workflow_id, timer_id, fire_at, armed_seq)
570 .await
571 {
572 Ok(TimerRetirement::Retired) => RetireAttempt::Retired,
573 Ok(TimerRetirement::Superseded) => RetireAttempt::Superseded,
574 Err(error) => {
575 tracing::warn!(
576 %workflow_id,
577 %timer_id,
578 %fire_at,
579 %error,
580 "consumed timer row could not be retired; the row survives until a \
581 later fire or boot/adoption sweep retires it"
582 );
583 RetireAttempt::Failed
584 }
585 }
586 }
587
588 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
589 let history = self.store.read_history(workflow_id).await?;
590 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
591 Ok(EventEnvelope {
592 seq,
593 recorded_at: (self.recorded_at)(),
594 workflow_id: workflow_id.clone(),
595 })
596 }
597}
598
599pub(crate) fn armed_fire_at_in_active_segment(
610 history: &[Event],
611 timer_id: &TimerId,
612) -> Option<DateTime<Utc>> {
613 let mut armed = None;
614 for event in active_segment(history) {
615 match event {
616 Event::TimerStarted {
617 timer_id: id,
618 fire_at,
619 ..
620 } if id == timer_id => {
621 armed = Some(*fire_at);
622 }
623 Event::TimerFired { timer_id: id, .. } | Event::TimerCancelled { timer_id: id, .. }
624 if id == timer_id =>
625 {
626 armed = None;
627 }
628 _ => {}
629 }
630 }
631 armed
632}
633
634fn last_recorded_arming(history: &[Event], timer_id: &TimerId) -> Option<(DateTime<Utc>, u64)> {
645 history.iter().rev().find_map(|event| match event {
646 Event::TimerStarted {
647 envelope,
648 timer_id: id,
649 fire_at,
650 } if id == timer_id => Some((*fire_at, envelope.seq)),
651 _ => None,
652 })
653}
654
655pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
671 let mut live: Vec<TimerId> = Vec::new();
672 for event in active_segment(history) {
673 match event {
674 Event::TimerStarted { timer_id, .. } if !live.contains(timer_id) => {
675 live.push(timer_id.clone());
676 }
677 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
678 live.retain(|id| id != timer_id);
679 }
680 _ => {}
681 }
682 }
683 live
684}
685
686fn active_segment(history: &[Event]) -> &[Event] {
695 let segment_start = history
696 .iter()
697 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
698 .unwrap_or(0);
699 &history[segment_start..]
700}
701
702#[derive(Clone, Copy, Debug, Eq, PartialEq)]
714pub(crate) enum TimerDisposition {
715 Live,
717 Fired,
722 Cancelled,
724 Absent,
727}
728
729pub(crate) fn timer_disposition_in_active_segment(
731 history: &[Event],
732 timer_id: &TimerId,
733) -> TimerDisposition {
734 let mut disposition = TimerDisposition::Absent;
735 for event in active_segment(history) {
736 match event {
737 Event::TimerStarted { timer_id: id, .. } if id == timer_id => {
738 disposition = TimerDisposition::Live;
739 }
740 Event::TimerFired { timer_id: id, .. } if id == timer_id => {
741 disposition = TimerDisposition::Fired;
742 }
743 Event::TimerCancelled { timer_id: id, .. } if id == timer_id => {
744 disposition = TimerDisposition::Cancelled;
745 }
746 _ => {}
747 }
748 }
749 disposition
750}
751
752#[cfg(test)]
753mod tests {
754 use std::sync::Arc;
755
756 use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
757 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
758 use chrono::{DateTime, Utc};
759
760 use super::{
761 TimerDisposition, TimerService, TimerServiceError, live_timers_in_active_segment,
762 timer_disposition_in_active_segment,
763 };
764 use crate::engine_seam::test_support::{
765 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
766 };
767 use crate::engine_seam::{
768 EngineHandle, EngineSeamError, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
769 };
770 use crate::time::deadline::{DeadlineHandler, DeadlineHandlerError, deadline_timer_id};
771
772 fn instant(offset_seconds: i64) -> DateTime<Utc> {
773 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
774 }
775
776 fn workflow_id() -> WorkflowId {
777 WorkflowId::new_v4()
778 }
779
780 fn timer_id() -> TimerId {
781 TimerId::anonymous(7)
782 }
783
784 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
785 let concrete_store = Arc::new(InMemoryStore::default());
786 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
787 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
788 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
789 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
790 (concrete_store, engine, service)
791 }
792
793 fn recorded_at() -> DateTime<Utc> {
794 instant(1)
795 }
796
797 async fn history(
798 store: &InMemoryStore,
799 workflow_id: &WorkflowId,
800 ) -> Result<Vec<Event>, StoreError> {
801 store.read_history(workflow_id).await
802 }
803
804 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
805 events
806 .iter()
807 .filter(|event| {
808 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
809 })
810 .count()
811 }
812
813 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
814 Event::TimerStarted {
815 envelope: EventEnvelope {
816 seq,
817 recorded_at: instant(0),
818 workflow_id: workflow_id.clone(),
819 },
820 timer_id: timer_id.clone(),
821 fire_at: instant(5),
822 }
823 }
824
825 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
826 Event::WorkflowStarted {
827 envelope: EventEnvelope {
828 seq,
829 recorded_at: instant(0),
830 workflow_id: workflow_id.clone(),
831 },
832 workflow_type: "fixture".to_owned(),
833 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
834 run_id: aion_core::RunId::new_v4(),
835 parent_run_id: None,
836 parent_workflow_id: None,
837 package_version: aion_core::PackageVersion::new("a".repeat(64)),
838 }
839 }
840
841 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
842 Event::TimerFired {
843 envelope: EventEnvelope {
844 seq,
845 recorded_at: instant(0),
846 workflow_id: workflow_id.clone(),
847 },
848 timer_id: timer_id.clone(),
849 }
850 }
851
852 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
853 Event::TimerCancelled {
854 cause: TimerCancelCause::WorkflowIntent,
855 envelope: EventEnvelope {
856 seq,
857 recorded_at: instant(0),
858 workflow_id: workflow_id.clone(),
859 },
860 timer_id: timer_id.clone(),
861 }
862 }
863
864 fn make_named(name: &str) -> TimerId {
865 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
869 }
870
871 fn named_timer_id() -> TimerId {
872 make_named("review-deadline")
873 }
874
875 #[test]
878 fn started_timer_is_live() {
879 let workflow_id = workflow_id();
880 let timer_id = named_timer_id();
881 let history = vec![
882 workflow_started_event(&workflow_id, 0),
883 timer_started_event(&workflow_id, &timer_id, 1),
884 ];
885 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
886 }
887
888 #[test]
889 fn started_then_fired_timer_is_dead() {
890 let workflow_id = workflow_id();
891 let timer_id = named_timer_id();
892 let history = vec![
893 workflow_started_event(&workflow_id, 0),
894 timer_started_event(&workflow_id, &timer_id, 1),
895 timer_fired_event(&workflow_id, &timer_id, 2),
896 ];
897 assert!(live_timers_in_active_segment(&history).is_empty());
898 }
899
900 #[test]
901 fn started_then_cancelled_timer_is_dead() {
902 let workflow_id = workflow_id();
903 let timer_id = named_timer_id();
904 let history = vec![
905 workflow_started_event(&workflow_id, 0),
906 timer_started_event(&workflow_id, &timer_id, 1),
907 timer_cancelled_event(&workflow_id, &timer_id, 2),
908 ];
909 assert!(live_timers_in_active_segment(&history).is_empty());
910 }
911
912 #[test]
913 fn restarted_named_timer_after_fire_is_live() {
914 let workflow_id = workflow_id();
918 let timer_id = named_timer_id();
919 let history = vec![
920 workflow_started_event(&workflow_id, 0),
921 timer_started_event(&workflow_id, &timer_id, 1),
922 timer_fired_event(&workflow_id, &timer_id, 2),
923 timer_started_event(&workflow_id, &timer_id, 3),
924 ];
925 assert_eq!(
926 live_timers_in_active_segment(&history),
927 vec![timer_id],
928 "a re-armed named timer is live again"
929 );
930 }
931
932 #[test]
933 fn restarted_named_timer_after_cancel_is_live() {
934 let workflow_id = workflow_id();
935 let timer_id = named_timer_id();
936 let history = vec![
937 workflow_started_event(&workflow_id, 0),
938 timer_started_event(&workflow_id, &timer_id, 1),
939 timer_cancelled_event(&workflow_id, &timer_id, 2),
940 timer_started_event(&workflow_id, &timer_id, 3),
941 ];
942 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
943 }
944
945 #[test]
946 fn prior_run_segment_timer_is_not_live() {
947 let workflow_id = workflow_id();
950 let prior = named_timer_id();
951 let current = make_named("current-deadline");
952 let history = vec![
953 workflow_started_event(&workflow_id, 0),
954 timer_started_event(&workflow_id, &prior, 1),
955 workflow_started_event(&workflow_id, 2),
957 timer_started_event(&workflow_id, ¤t, 3),
958 ];
959 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
960 }
961
962 #[test]
967 fn disposition_tracks_the_last_event_for_the_id_and_agrees_with_liveness() {
968 let workflow_id = workflow_id();
969 let timer_id = named_timer_id();
970 let other = make_named("unrelated");
971 let assert_agrees = |history: &[Event], expected: TimerDisposition| {
972 assert_eq!(
973 timer_disposition_in_active_segment(history, &timer_id),
974 expected
975 );
976 assert_eq!(
977 live_timers_in_active_segment(history).contains(&timer_id),
978 expected == TimerDisposition::Live,
979 "the per-id disposition and the enumerating model disagree on liveness"
980 );
981 };
982
983 let mut history = vec![
985 workflow_started_event(&workflow_id, 0),
986 timer_started_event(&workflow_id, &timer_id, 1),
987 ];
988 assert_agrees(&history, TimerDisposition::Live);
989
990 history.push(timer_fired_event(&workflow_id, &timer_id, 2));
992 assert_agrees(&history, TimerDisposition::Fired);
993
994 history.push(timer_fired_event(&workflow_id, &other, 3));
996 assert_agrees(&history, TimerDisposition::Fired);
997
998 history.push(timer_started_event(&workflow_id, &timer_id, 4));
1000 assert_agrees(&history, TimerDisposition::Live);
1001
1002 history.push(timer_cancelled_event(&workflow_id, &timer_id, 5));
1004 assert_agrees(&history, TimerDisposition::Cancelled);
1005
1006 history.push(workflow_started_event(&workflow_id, 6));
1009 assert_agrees(&history, TimerDisposition::Absent);
1010
1011 assert_agrees(&[], TimerDisposition::Absent);
1013 }
1014
1015 #[tokio::test]
1016 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
1017 let process = WorkflowProcessHandle::new(42);
1022 let (store, engine, service) = service();
1023 let workflow_id = workflow_id();
1024 let timer_id = named_timer_id();
1025 let fire_at = instant(110);
1026 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1027 engine.record_workflow_event(
1028 &workflow_id,
1029 timer_started_event(&workflow_id, &timer_id, 1),
1030 )?;
1031 engine
1032 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1033 engine.record_workflow_event(
1034 &workflow_id,
1035 timer_started_event(&workflow_id, &timer_id, 3),
1036 )?;
1037
1038 service
1039 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1040 .await?;
1041
1042 assert_eq!(
1043 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1044 2,
1045 "the re-armed timer fires again, recording a second TimerFired"
1046 );
1047 assert_eq!(engine.delivered_messages()?.len(), 1);
1048 Ok(())
1049 }
1050
1051 fn workflow_completed_event(workflow_id: &WorkflowId, seq: u64) -> Event {
1052 Event::WorkflowCompleted {
1053 envelope: EventEnvelope {
1054 seq,
1055 recorded_at: instant(0),
1056 workflow_id: workflow_id.clone(),
1057 },
1058 result: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
1059 }
1060 }
1061
1062 fn process_gone() -> EngineSeamError {
1063 EngineSeamError::Delivery {
1064 reason: "runtime error: process 42 is not live".to_owned(),
1065 }
1066 }
1067
1068 #[tokio::test]
1074 async fn a_wake_that_finds_the_process_gone_owes_nothing_when_the_run_is_terminal()
1075 -> Result<(), TimerServiceError> {
1076 let process = WorkflowProcessHandle::new(42);
1077 let (store, engine, service) = service();
1078 let workflow_id = workflow_id();
1079 let timer_id = named_timer_id();
1080 let fire_at = instant(110);
1081 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1082 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 1))?;
1083 engine.record_workflow_event(
1084 &workflow_id,
1085 timer_started_event(&workflow_id, &timer_id, 2),
1086 )?;
1087 engine.record_workflow_event(&workflow_id, workflow_completed_event(&workflow_id, 3))?;
1091 service
1092 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 2)
1093 .await?;
1094 engine.push_delivery_response(Err(process_gone()))?;
1095
1096 service
1097 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1098 .await?;
1099
1100 assert_eq!(
1101 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1102 1,
1103 "the fire was recorded before the wake was attempted"
1104 );
1105 assert!(
1106 store.expired_timers(fire_at).await?.is_empty(),
1107 "a fire owed to a finished run consumes its arming: the row retires"
1108 );
1109 Ok(())
1110 }
1111
1112 #[tokio::test]
1116 async fn a_wake_that_finds_the_process_gone_stays_an_error_while_the_run_is_active()
1117 -> Result<(), TimerServiceError> {
1118 let process = WorkflowProcessHandle::new(42);
1119 let (store, engine, service) = service();
1120 let workflow_id = workflow_id();
1121 let timer_id = named_timer_id();
1122 let fire_at = instant(110);
1123 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1124 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 1))?;
1125 engine.record_workflow_event(
1126 &workflow_id,
1127 timer_started_event(&workflow_id, &timer_id, 2),
1128 )?;
1129 service
1130 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 2)
1131 .await?;
1132 engine.push_delivery_response(Err(process_gone()))?;
1133
1134 let outcome = service
1135 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1136 .await;
1137
1138 assert!(
1139 matches!(
1140 outcome,
1141 Err(TimerServiceError::Engine(EngineSeamError::Delivery { .. }))
1142 ),
1143 "an active run with a dead process is a fault the sweep must not absorb: {outcome:?}"
1144 );
1145 assert_eq!(
1146 store.expired_timers(fire_at).await?.len(),
1147 1,
1148 "the row survives for the next sweep's redelivery"
1149 );
1150 Ok(())
1151 }
1152
1153 #[tokio::test]
1154 async fn schedule_records_timer_row_without_timer_started_event()
1155 -> Result<(), TimerServiceError> {
1156 let (store, _engine, service) = service();
1157 let workflow_id = workflow_id();
1158 let timer_id = timer_id();
1159 let fire_at = instant(10);
1160
1161 service
1162 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1163 .await?;
1164
1165 let expired = store.expired_timers(fire_at).await?;
1166 assert_eq!(expired.len(), 1);
1167 assert_eq!(expired[0].workflow_id, workflow_id);
1168 assert_eq!(expired[0].timer_id, timer_id);
1169 assert_eq!(expired[0].fire_at, fire_at);
1170
1171 assert!(history(&store, &workflow_id).await?.is_empty());
1172 Ok(())
1173 }
1174
1175 #[tokio::test]
1176 async fn schedule_arms_wheel_for_resident_workflow() -> 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(20);
1182 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1183
1184 service
1185 .schedule(workflow_id, timer_id.clone(), fire_at, 1)
1186 .await?;
1187
1188 assert_eq!(
1189 engine.armed_timers()?,
1190 vec![TimerWheelEntry {
1191 process,
1192 timer_id,
1193 fire_at
1194 }]
1195 );
1196 Ok(())
1197 }
1198
1199 #[tokio::test]
1200 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
1201 let (store, engine, service) = service();
1202 let workflow_id = workflow_id();
1203 let timer_id = timer_id();
1204 let fire_at = instant(30);
1205 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1206
1207 service
1208 .schedule(workflow_id.clone(), timer_id, fire_at, 1)
1209 .await?;
1210
1211 assert!(engine.armed_timers()?.is_empty());
1212 assert!(history(&store, &workflow_id).await?.is_empty());
1213 Ok(())
1214 }
1215
1216 #[tokio::test]
1217 async fn fire_records_timer_fired_then_delivers_mailbox_message()
1218 -> Result<(), TimerServiceError> {
1219 let process = WorkflowProcessHandle::new(42);
1220 let (store, engine, service) = service();
1221 let workflow_id = workflow_id();
1222 let timer_id = timer_id();
1223 let fire_at = instant(40);
1224 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1225 engine.record_workflow_event(
1226 &workflow_id,
1227 timer_started_event(&workflow_id, &timer_id, 1),
1228 )?;
1229
1230 service
1231 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1232 .await?;
1233
1234 assert_eq!(
1235 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1236 1
1237 );
1238 assert_eq!(
1239 engine.delivered_messages()?,
1240 vec![(
1241 process,
1242 DeliveredWorkflowMessage::TimerFired {
1243 timer_id: timer_id.clone(),
1244 fire_at
1245 }
1246 )]
1247 );
1248 assert!(matches!(
1249 engine.operations()?.as_slice(),
1250 [
1251 FakeEngineOperation::EventRecorded {
1252 event: Event::TimerStarted { .. },
1253 ..
1254 },
1255 FakeEngineOperation::EventRecorded {
1256 workflow_id: recorded_workflow_id,
1257 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
1258 },
1259 FakeEngineOperation::Delivered {
1260 process: delivered_process,
1261 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
1262 }
1263 ] if recorded_workflow_id == &workflow_id
1264 && recorded_timer_id == &timer_id
1265 && delivered_process == &process
1266 && delivered_timer_id == &timer_id
1267 ));
1268 Ok(())
1269 }
1270
1271 #[tokio::test]
1272 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
1273 -> Result<(), TimerServiceError> {
1274 let (store, engine, service) = service();
1275 let workflow_id = workflow_id();
1276 let timer_id = timer_id();
1277 let fire_at = instant(50);
1278 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1279 engine.record_workflow_event(
1280 &workflow_id,
1281 timer_started_event(&workflow_id, &timer_id, 1),
1282 )?;
1283
1284 service
1285 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1286 .await?;
1287
1288 assert_eq!(
1289 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1290 1
1291 );
1292 assert!(engine.delivered_messages()?.is_empty());
1293 Ok(())
1294 }
1295
1296 #[tokio::test]
1305 async fn firing_same_timer_twice_records_once_and_redelivers_the_wake()
1306 -> Result<(), TimerServiceError> {
1307 let process = WorkflowProcessHandle::new(42);
1308 let (store, engine, service) = service();
1309 let workflow_id = workflow_id();
1310 let timer_id = timer_id();
1311 let fire_at = instant(60);
1312 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1313 engine.record_workflow_event(
1314 &workflow_id,
1315 timer_started_event(&workflow_id, &timer_id, 1),
1316 )?;
1317
1318 service
1319 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1320 .await?;
1321 service
1327 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1328 .await?;
1329
1330 assert_eq!(
1331 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1332 1,
1333 "the recorded fire must never be appended a second time"
1334 );
1335 assert_eq!(
1336 engine.delivered_messages()?.len(),
1337 2,
1338 "the second fire re-delivers the owed wake instead of silently no-opping"
1339 );
1340 Ok(())
1341 }
1342
1343 #[tokio::test]
1351 async fn already_recorded_fire_delivers_owed_wake_without_second_append()
1352 -> Result<(), TimerServiceError> {
1353 let process = WorkflowProcessHandle::new(42);
1354 let (store, engine, service) = service();
1355 let workflow_id = workflow_id();
1356 let timer_id = timer_id();
1357 let fire_at = instant(140);
1358 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1359 engine.record_workflow_event(
1360 &workflow_id,
1361 timer_started_event(&workflow_id, &timer_id, 1),
1362 )?;
1363 engine
1364 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1365
1366 service
1367 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1368 .await?;
1369
1370 assert_eq!(
1371 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1372 1,
1373 "the already-recorded fire must not be appended again"
1374 );
1375 assert_eq!(
1376 engine.delivered_messages()?,
1377 vec![(
1378 process,
1379 DeliveredWorkflowMessage::TimerFired { timer_id, fire_at }
1380 )],
1381 "the owed mailbox wake must be delivered"
1382 );
1383 Ok(())
1384 }
1385
1386 #[tokio::test]
1393 async fn already_recorded_fire_for_nonresident_workflow_wakes_nothing()
1394 -> Result<(), TimerServiceError> {
1395 let (store, engine, service) = service();
1396 let workflow_id = workflow_id();
1397 let timer_id = timer_id();
1398 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1399 engine.record_workflow_event(
1400 &workflow_id,
1401 timer_started_event(&workflow_id, &timer_id, 1),
1402 )?;
1403 engine
1404 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1405
1406 service
1407 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(150))
1408 .await?;
1409
1410 assert_eq!(
1411 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1412 1,
1413 "a non-resident redelivery must not re-enter the recorder seam"
1414 );
1415 assert!(
1416 engine.delivered_messages()?.is_empty(),
1417 "no wake is attempted for a non-resident workflow"
1418 );
1419 Ok(())
1420 }
1421
1422 #[tokio::test]
1428 async fn already_recorded_fire_after_run_terminal_delivers_no_wake()
1429 -> Result<(), TimerServiceError> {
1430 let process = WorkflowProcessHandle::new(42);
1431 let (store, engine, service) = service();
1432 let workflow_id = workflow_id();
1433 let timer_id = timer_id();
1434 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1435 engine.record_workflow_event(
1436 &workflow_id,
1437 timer_started_event(&workflow_id, &timer_id, 1),
1438 )?;
1439 engine
1440 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1441 engine.refuse_next_record_as_terminal()?;
1442
1443 service
1444 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(160))
1445 .await?;
1446
1447 assert_eq!(
1448 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1449 1
1450 );
1451 assert!(
1452 engine.delivered_messages()?.is_empty(),
1453 "a post-terminal redelivery must not wake the terminated run"
1454 );
1455 Ok(())
1456 }
1457
1458 #[tokio::test]
1459 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
1460 let process = WorkflowProcessHandle::new(42);
1461 let (store, engine, service) = service();
1462 let workflow_id = workflow_id();
1463 let timer_id = timer_id();
1464 let fire_at = instant(70);
1465 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1466 engine.record_workflow_event(
1467 &workflow_id,
1468 timer_started_event(&workflow_id, &timer_id, 1),
1469 )?;
1470 let cancelled = Event::TimerCancelled {
1471 cause: TimerCancelCause::WorkflowIntent,
1472 envelope: EventEnvelope {
1473 seq: 2,
1474 recorded_at: instant(69),
1475 workflow_id: workflow_id.clone(),
1476 },
1477 timer_id: timer_id.clone(),
1478 };
1479 engine.record_workflow_event(&workflow_id, cancelled)?;
1480
1481 service
1482 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1483 .await?;
1484
1485 let history = history(&store, &workflow_id).await?;
1486 assert_eq!(count_timer_fired(&history, &timer_id), 0);
1487 assert!(engine.delivered_messages()?.is_empty());
1488 Ok(())
1489 }
1490
1491 #[tokio::test]
1492 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
1493 let process = WorkflowProcessHandle::new(42);
1494 let (store, engine, service) = service();
1495 let workflow_id = workflow_id();
1496 let timer_id = timer_id();
1497 let fire_at = instant(80);
1498
1499 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1500 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1501 engine.record_workflow_event(
1502 &workflow_id,
1503 timer_started_event(&workflow_id, &timer_id, 1),
1504 )?;
1505 service
1506 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1507 .await?;
1508
1509 assert_eq!(
1510 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1511 1
1512 );
1513 assert!(engine.delivered_messages()?.is_empty());
1514 Ok(())
1515 }
1516
1517 #[tokio::test]
1518 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
1519 let process = WorkflowProcessHandle::new(42);
1520 let (store, engine, service) = service();
1521 let workflow_id = workflow_id();
1522 let timer_id = timer_id();
1523 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1524
1525 service
1526 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
1527 .await?;
1528
1529 assert!(history(&store, &workflow_id).await?.is_empty());
1530 assert!(engine.delivered_messages()?.is_empty());
1531 Ok(())
1532 }
1533
1534 struct RecordingDeadlineHandler {
1536 calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
1537 fail: bool,
1538 }
1539
1540 impl RecordingDeadlineHandler {
1541 fn new(fail: bool) -> Self {
1542 Self {
1543 calls: std::sync::Mutex::new(Vec::new()),
1544 fail,
1545 }
1546 }
1547
1548 fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
1549 self.calls
1550 .lock()
1551 .map(|calls| calls.clone())
1552 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
1553 }
1554 }
1555
1556 #[async_trait::async_trait]
1557 impl DeadlineHandler for RecordingDeadlineHandler {
1558 async fn on_deadline_elapsed(
1559 &self,
1560 workflow_id: WorkflowId,
1561 run_id: RunId,
1562 ) -> Result<(), DeadlineHandlerError> {
1563 self.calls
1564 .lock()
1565 .map_err(|error| DeadlineHandlerError(error.to_string()))?
1566 .push((workflow_id, run_id));
1567 if self.fail {
1568 Err(DeadlineHandlerError(
1569 "deliberate handler failure".to_owned(),
1570 ))
1571 } else {
1572 Ok(())
1573 }
1574 }
1575 }
1576
1577 fn service_with_handler(
1578 handler: Arc<dyn DeadlineHandler>,
1579 ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
1580 let concrete_store = Arc::new(InMemoryStore::default());
1581 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1582 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
1583 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1584 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
1585 .with_deadline_handler(handler);
1586 (concrete_store, engine, service)
1587 }
1588
1589 #[tokio::test]
1592 async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
1593 -> Result<(), TimerServiceError> {
1594 let run_id = RunId::new_v4();
1595 let deadline_id = deadline_timer_id(&run_id)
1596 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1597 let handler = Arc::new(RecordingDeadlineHandler::new(false));
1598 let (store, engine, service) = service_with_handler(handler.clone());
1599 let workflow_id = workflow_id();
1600 let fire_at = instant(120);
1601 engine.set_residency(
1602 workflow_id.clone(),
1603 WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
1604 )?;
1605 engine.record_workflow_event(
1606 &workflow_id,
1607 timer_started_event(&workflow_id, &deadline_id, 1),
1608 )?;
1609
1610 service
1611 .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
1612 .await?;
1613
1614 assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
1615 assert_eq!(
1616 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1617 0,
1618 "a deadline fire never records TimerFired"
1619 );
1620 assert!(engine.delivered_messages()?.is_empty());
1621 Ok(())
1622 }
1623
1624 #[tokio::test]
1627 async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
1628 let run_id = RunId::new_v4();
1629 let deadline_id = deadline_timer_id(&run_id)
1630 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1631 let (store, engine, service) = service();
1632 let workflow_id = workflow_id();
1633 engine.record_workflow_event(
1634 &workflow_id,
1635 timer_started_event(&workflow_id, &deadline_id, 1),
1636 )?;
1637
1638 let result = service
1639 .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
1640 .await;
1641
1642 assert!(
1643 matches!(result, Err(TimerServiceError::Deadline(_))),
1644 "unhandled deadline fire must be a typed error, got {result:?}"
1645 );
1646 assert_eq!(
1647 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1648 0
1649 );
1650 Ok(())
1651 }
1652
1653 #[tokio::test]
1655 async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1656 let run_id = RunId::new_v4();
1657 let deadline_id = deadline_timer_id(&run_id)
1658 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1659 let handler = Arc::new(RecordingDeadlineHandler::new(true));
1660 let (_store, engine, service) = service_with_handler(handler);
1661 let workflow_id = workflow_id();
1662 engine.record_workflow_event(
1663 &workflow_id,
1664 timer_started_event(&workflow_id, &deadline_id, 1),
1665 )?;
1666
1667 let result = service
1668 .fire_timer(workflow_id, deadline_id, instant(120))
1669 .await;
1670
1671 assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1672 Ok(())
1673 }
1674
1675 #[tokio::test]
1681 async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1682 -> Result<(), TimerServiceError> {
1683 let process = WorkflowProcessHandle::new(42);
1684 let (store, engine, service) = service();
1685 let workflow_id = workflow_id();
1686 let timer_id = timer_id();
1687 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1688 engine.record_workflow_event(
1689 &workflow_id,
1690 timer_started_event(&workflow_id, &timer_id, 1),
1691 )?;
1692 engine.refuse_next_record_as_terminal()?;
1693
1694 service
1695 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1696 .await?;
1697
1698 assert_eq!(
1699 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1700 0,
1701 "a refused fire records no TimerFired"
1702 );
1703 assert!(
1704 engine.delivered_messages()?.is_empty(),
1705 "a refused fire delivers no wake"
1706 );
1707 Ok(())
1708 }
1709
1710 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1718 async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1719 -> Result<(), TimerServiceError> {
1720 use dashmap::DashSet;
1721 use tokio::sync::Barrier;
1722
1723 for _ in 0..20 {
1724 let process = WorkflowProcessHandle::new(42);
1725 let concrete_store = Arc::new(InMemoryStore::default());
1726 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1727 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1728 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1729 let coordinator = Arc::new(DashSet::new());
1730 let service_a =
1731 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1732 .with_terminal_updates(Arc::clone(&coordinator));
1733 let service_b =
1734 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1735 .with_terminal_updates(Arc::clone(&coordinator));
1736
1737 let workflow_id = workflow_id();
1738 let timer_id = timer_id();
1739 let fire_at = instant(200);
1740 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1741 engine.record_workflow_event(
1742 &workflow_id,
1743 timer_started_event(&workflow_id, &timer_id, 1),
1744 )?;
1745
1746 let gate = Arc::new(Barrier::new(2));
1747 let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1748 let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1749 let cancel = async move {
1750 cancel_gate.wait().await;
1751 service_a
1752 .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1753 .await
1754 };
1755 let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1756 let fire = async move {
1757 fire_gate.wait().await;
1758 service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1759 };
1760 let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1761 cancel_result?;
1762 fire_result?;
1763
1764 let history = history(&concrete_store, &workflow_id).await?;
1765 let terminal_timer_events = history
1766 .iter()
1767 .filter(|event| {
1768 matches!(
1769 event,
1770 Event::TimerFired { timer_id: recorded, .. }
1771 | Event::TimerCancelled { timer_id: recorded, .. }
1772 if recorded == &timer_id
1773 )
1774 })
1775 .count();
1776 assert_eq!(
1777 terminal_timer_events, 1,
1778 "first-recorded wins across shared services: {history:#?}"
1779 );
1780 }
1781 Ok(())
1782 }
1783
1784 #[tokio::test]
1785 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1786 {
1787 let process = WorkflowProcessHandle::new(42);
1788 let (store, engine, service) = service();
1789 let workflow_id = workflow_id();
1790 let timer_id = timer_id();
1791 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1792 engine.record_workflow_event(
1794 &workflow_id,
1795 timer_started_event(&workflow_id, &timer_id, 1),
1796 )?;
1797 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1798
1799 service
1800 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1801 .await?;
1802
1803 assert_eq!(
1804 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1805 0
1806 );
1807 assert!(engine.delivered_messages()?.is_empty());
1808 Ok(())
1809 }
1810
1811 async fn outstanding_rows(
1816 store: &InMemoryStore,
1817 as_of: DateTime<Utc>,
1818 ) -> Result<usize, StoreError> {
1819 Ok(store.expired_timers(as_of).await?.len())
1820 }
1821
1822 #[tokio::test]
1826 async fn a_recorded_fire_retires_the_consumed_row() -> Result<(), TimerServiceError> {
1827 let process = WorkflowProcessHandle::new(42);
1828 let (store, engine, service) = service();
1829 let workflow_id = workflow_id();
1830 let timer_id = timer_id();
1831 let fire_at = instant(40);
1832 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1833 engine.record_workflow_event(
1834 &workflow_id,
1835 timer_started_event(&workflow_id, &timer_id, 1),
1836 )?;
1837 service
1838 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1839 .await?;
1840 assert_eq!(
1841 outstanding_rows(&store, instant(1_000)).await?,
1842 1,
1843 "precondition: the arming's row is durable before the fire"
1844 );
1845
1846 service
1847 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1848 .await?;
1849
1850 assert_eq!(
1851 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1852 1,
1853 "the fire itself must still record"
1854 );
1855 assert_eq!(
1856 outstanding_rows(&store, instant(1_000)).await?,
1857 0,
1858 "a recorded fire must retire the consumed arming's row"
1859 );
1860 Ok(())
1861 }
1862
1863 #[tokio::test]
1866 async fn a_recorded_cancel_retires_the_consumed_row() -> Result<(), TimerServiceError> {
1867 let process = WorkflowProcessHandle::new(42);
1868 let (store, engine, service) = service();
1869 let workflow_id = workflow_id();
1870 let timer_id = named_timer_id();
1871 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1872 engine.record_workflow_event(
1876 &workflow_id,
1877 timer_started_event(&workflow_id, &timer_id, 1),
1878 )?;
1879 service
1880 .schedule(workflow_id.clone(), timer_id.clone(), instant(5), 1)
1881 .await?;
1882 assert_eq!(outstanding_rows(&store, instant(1_000)).await?, 1);
1883
1884 service
1885 .cancel(
1886 workflow_id.clone(),
1887 timer_id.clone(),
1888 TimerCancelCause::WorkflowIntent,
1889 )
1890 .await?;
1891
1892 assert_eq!(
1893 outstanding_rows(&store, instant(1_000)).await?,
1894 0,
1895 "a recorded cancel must retire the cancelled arming's row"
1896 );
1897 Ok(())
1898 }
1899
1900 #[tokio::test]
1906 async fn a_stale_fire_leaves_a_re_armed_timers_row() -> Result<(), TimerServiceError> {
1907 let process = WorkflowProcessHandle::new(42);
1908 let (store, engine, service) = service();
1909 let workflow_id = workflow_id();
1910 let timer_id = named_timer_id();
1911 let old_fire_at = instant(5);
1912 let new_fire_at = instant(500);
1913 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1914 engine.record_workflow_event(
1915 &workflow_id,
1916 timer_started_event(&workflow_id, &timer_id, 1),
1917 )?;
1918 service
1920 .schedule(workflow_id.clone(), timer_id.clone(), new_fire_at, 2)
1921 .await?;
1922
1923 service
1925 .fire_timer(workflow_id.clone(), timer_id.clone(), old_fire_at)
1926 .await?;
1927
1928 assert_eq!(
1929 outstanding_rows(&store, instant(1_000)).await?,
1930 1,
1931 "the re-armed row is the replacement arming's only durable claim \
1932 to a recovery fire; a stale retire must leave it standing"
1933 );
1934 Ok(())
1935 }
1936
1937 #[tokio::test]
1941 async fn a_terminal_refused_fire_retires_the_row() -> Result<(), TimerServiceError> {
1942 let process = WorkflowProcessHandle::new(42);
1943 let (store, engine, service) = service();
1944 let workflow_id = workflow_id();
1945 let timer_id = timer_id();
1946 let fire_at = instant(40);
1947 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1948 engine.record_workflow_event(
1949 &workflow_id,
1950 timer_started_event(&workflow_id, &timer_id, 1),
1951 )?;
1952 service
1953 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1954 .await?;
1955 engine.refuse_next_record_as_terminal()?;
1956
1957 service
1958 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1959 .await?;
1960
1961 assert_eq!(
1962 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1963 0,
1964 "the refused fire must record nothing"
1965 );
1966 assert!(
1967 engine.delivered_messages()?.is_empty(),
1968 "a refused fire must wake nothing"
1969 );
1970 assert_eq!(
1971 outstanding_rows(&store, instant(1_000)).await?,
1972 0,
1973 "a terminal-refused fire's arming is moot forever; its row retires"
1974 );
1975 Ok(())
1976 }
1977}