1use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, TimerCancelCause, TimerId, WorkflowId};
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 self.engine.deliver_workflow_message(
384 process,
385 WorkflowMailboxMessage::TimerFired {
386 timer_id: timer_id.clone(),
387 fire_at,
388 },
389 )?;
390 }
391
392 self.retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
397 .await;
398
399 Ok(())
400 }
401
402 pub(crate) async fn redeliver_owed_wake(
433 &self,
434 workflow_id: WorkflowId,
435 timer_id: TimerId,
436 fire_at: DateTime<Utc>,
437 armed_seq: u64,
438 ) -> Result<(bool, RetireAttempt), TimerServiceError> {
439 let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)?
440 else {
441 let row = self
446 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
447 .await;
448 return Ok((false, row));
449 };
450
451 match self
452 .engine
453 .record_redelivered_timer_fire(&workflow_id, &timer_id)?
454 {
455 RedeliveredFire::RefusedTerminal | RedeliveredFire::NotOwed => {
461 let row = self
462 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
463 .await;
464 Ok((false, row))
465 }
466 RedeliveredFire::WakeOwed => {
467 self.engine.deliver_workflow_message(
468 process,
469 WorkflowMailboxMessage::TimerFired {
470 timer_id: timer_id.clone(),
471 fire_at,
472 },
473 )?;
474 tracing::info!(
475 %workflow_id,
476 %timer_id,
477 "timer fire was already durably recorded; delivered the owed mailbox wake"
478 );
479 let row = self
480 .retire_consumed_row(&workflow_id, &timer_id, fire_at, armed_seq)
481 .await;
482 Ok((true, row))
483 }
484 }
485 }
486
487 async fn fire_deadline(
496 &self,
497 workflow_id: WorkflowId,
498 timer_id: TimerId,
499 ) -> Result<(), TimerServiceError> {
500 let handler = self.deadline_handler.as_ref().ok_or_else(|| {
501 TimerServiceError::Deadline(format!(
502 "no deadline handler registered for {timer_id} on workflow {workflow_id}"
503 ))
504 })?;
505 let run_id = deadline_run_id(&timer_id).ok_or_else(|| {
506 TimerServiceError::Deadline(format!(
507 "malformed deadline timer {timer_id} on workflow {workflow_id}"
508 ))
509 })?;
510 handler
511 .on_deadline_elapsed(workflow_id, run_id)
512 .await
513 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
514 }
515
516 pub(crate) async fn retire_consumed_row(
535 &self,
536 workflow_id: &WorkflowId,
537 timer_id: &TimerId,
538 fire_at: DateTime<Utc>,
539 armed_seq: u64,
540 ) -> RetireAttempt {
541 match self
542 .store
543 .retire_timer(workflow_id, timer_id, fire_at, armed_seq)
544 .await
545 {
546 Ok(TimerRetirement::Retired) => RetireAttempt::Retired,
547 Ok(TimerRetirement::Superseded) => RetireAttempt::Superseded,
548 Err(error) => {
549 tracing::warn!(
550 %workflow_id,
551 %timer_id,
552 %fire_at,
553 %error,
554 "consumed timer row could not be retired; the row survives until a \
555 later fire or boot/adoption sweep retires it"
556 );
557 RetireAttempt::Failed
558 }
559 }
560 }
561
562 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
563 let history = self.store.read_history(workflow_id).await?;
564 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
565 Ok(EventEnvelope {
566 seq,
567 recorded_at: (self.recorded_at)(),
568 workflow_id: workflow_id.clone(),
569 })
570 }
571}
572
573pub(crate) fn armed_fire_at_in_active_segment(
584 history: &[Event],
585 timer_id: &TimerId,
586) -> Option<DateTime<Utc>> {
587 let mut armed = None;
588 for event in active_segment(history) {
589 match event {
590 Event::TimerStarted {
591 timer_id: id,
592 fire_at,
593 ..
594 } if id == timer_id => {
595 armed = Some(*fire_at);
596 }
597 Event::TimerFired { timer_id: id, .. } | Event::TimerCancelled { timer_id: id, .. }
598 if id == timer_id =>
599 {
600 armed = None;
601 }
602 _ => {}
603 }
604 }
605 armed
606}
607
608fn last_recorded_arming(history: &[Event], timer_id: &TimerId) -> Option<(DateTime<Utc>, u64)> {
619 history.iter().rev().find_map(|event| match event {
620 Event::TimerStarted {
621 envelope,
622 timer_id: id,
623 fire_at,
624 } if id == timer_id => Some((*fire_at, envelope.seq)),
625 _ => None,
626 })
627}
628
629pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
645 let mut live: Vec<TimerId> = Vec::new();
646 for event in active_segment(history) {
647 match event {
648 Event::TimerStarted { timer_id, .. } if !live.contains(timer_id) => {
649 live.push(timer_id.clone());
650 }
651 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
652 live.retain(|id| id != timer_id);
653 }
654 _ => {}
655 }
656 }
657 live
658}
659
660fn active_segment(history: &[Event]) -> &[Event] {
669 let segment_start = history
670 .iter()
671 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
672 .unwrap_or(0);
673 &history[segment_start..]
674}
675
676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
688pub(crate) enum TimerDisposition {
689 Live,
691 Fired,
696 Cancelled,
698 Absent,
701}
702
703pub(crate) fn timer_disposition_in_active_segment(
705 history: &[Event],
706 timer_id: &TimerId,
707) -> TimerDisposition {
708 let mut disposition = TimerDisposition::Absent;
709 for event in active_segment(history) {
710 match event {
711 Event::TimerStarted { timer_id: id, .. } if id == timer_id => {
712 disposition = TimerDisposition::Live;
713 }
714 Event::TimerFired { timer_id: id, .. } if id == timer_id => {
715 disposition = TimerDisposition::Fired;
716 }
717 Event::TimerCancelled { timer_id: id, .. } if id == timer_id => {
718 disposition = TimerDisposition::Cancelled;
719 }
720 _ => {}
721 }
722 }
723 disposition
724}
725
726#[cfg(test)]
727mod tests {
728 use std::sync::Arc;
729
730 use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
731 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
732 use chrono::{DateTime, Utc};
733
734 use super::{
735 TimerDisposition, TimerService, TimerServiceError, live_timers_in_active_segment,
736 timer_disposition_in_active_segment,
737 };
738 use crate::engine_seam::test_support::{
739 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
740 };
741 use crate::engine_seam::{
742 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
743 };
744 use crate::time::deadline::{DeadlineHandler, DeadlineHandlerError, deadline_timer_id};
745
746 fn instant(offset_seconds: i64) -> DateTime<Utc> {
747 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
748 }
749
750 fn workflow_id() -> WorkflowId {
751 WorkflowId::new_v4()
752 }
753
754 fn timer_id() -> TimerId {
755 TimerId::anonymous(7)
756 }
757
758 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
759 let concrete_store = Arc::new(InMemoryStore::default());
760 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
761 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
762 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
763 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
764 (concrete_store, engine, service)
765 }
766
767 fn recorded_at() -> DateTime<Utc> {
768 instant(1)
769 }
770
771 async fn history(
772 store: &InMemoryStore,
773 workflow_id: &WorkflowId,
774 ) -> Result<Vec<Event>, StoreError> {
775 store.read_history(workflow_id).await
776 }
777
778 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
779 events
780 .iter()
781 .filter(|event| {
782 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
783 })
784 .count()
785 }
786
787 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
788 Event::TimerStarted {
789 envelope: EventEnvelope {
790 seq,
791 recorded_at: instant(0),
792 workflow_id: workflow_id.clone(),
793 },
794 timer_id: timer_id.clone(),
795 fire_at: instant(5),
796 }
797 }
798
799 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
800 Event::WorkflowStarted {
801 envelope: EventEnvelope {
802 seq,
803 recorded_at: instant(0),
804 workflow_id: workflow_id.clone(),
805 },
806 workflow_type: "fixture".to_owned(),
807 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
808 run_id: aion_core::RunId::new_v4(),
809 parent_run_id: None,
810 parent_workflow_id: None,
811 package_version: aion_core::PackageVersion::new("a".repeat(64)),
812 }
813 }
814
815 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
816 Event::TimerFired {
817 envelope: EventEnvelope {
818 seq,
819 recorded_at: instant(0),
820 workflow_id: workflow_id.clone(),
821 },
822 timer_id: timer_id.clone(),
823 }
824 }
825
826 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
827 Event::TimerCancelled {
828 cause: TimerCancelCause::WorkflowIntent,
829 envelope: EventEnvelope {
830 seq,
831 recorded_at: instant(0),
832 workflow_id: workflow_id.clone(),
833 },
834 timer_id: timer_id.clone(),
835 }
836 }
837
838 fn make_named(name: &str) -> TimerId {
839 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
843 }
844
845 fn named_timer_id() -> TimerId {
846 make_named("review-deadline")
847 }
848
849 #[test]
852 fn started_timer_is_live() {
853 let workflow_id = workflow_id();
854 let timer_id = named_timer_id();
855 let history = vec![
856 workflow_started_event(&workflow_id, 0),
857 timer_started_event(&workflow_id, &timer_id, 1),
858 ];
859 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
860 }
861
862 #[test]
863 fn started_then_fired_timer_is_dead() {
864 let workflow_id = workflow_id();
865 let timer_id = named_timer_id();
866 let history = vec![
867 workflow_started_event(&workflow_id, 0),
868 timer_started_event(&workflow_id, &timer_id, 1),
869 timer_fired_event(&workflow_id, &timer_id, 2),
870 ];
871 assert!(live_timers_in_active_segment(&history).is_empty());
872 }
873
874 #[test]
875 fn started_then_cancelled_timer_is_dead() {
876 let workflow_id = workflow_id();
877 let timer_id = named_timer_id();
878 let history = vec![
879 workflow_started_event(&workflow_id, 0),
880 timer_started_event(&workflow_id, &timer_id, 1),
881 timer_cancelled_event(&workflow_id, &timer_id, 2),
882 ];
883 assert!(live_timers_in_active_segment(&history).is_empty());
884 }
885
886 #[test]
887 fn restarted_named_timer_after_fire_is_live() {
888 let workflow_id = workflow_id();
892 let timer_id = named_timer_id();
893 let history = vec![
894 workflow_started_event(&workflow_id, 0),
895 timer_started_event(&workflow_id, &timer_id, 1),
896 timer_fired_event(&workflow_id, &timer_id, 2),
897 timer_started_event(&workflow_id, &timer_id, 3),
898 ];
899 assert_eq!(
900 live_timers_in_active_segment(&history),
901 vec![timer_id],
902 "a re-armed named timer is live again"
903 );
904 }
905
906 #[test]
907 fn restarted_named_timer_after_cancel_is_live() {
908 let workflow_id = workflow_id();
909 let timer_id = named_timer_id();
910 let history = vec![
911 workflow_started_event(&workflow_id, 0),
912 timer_started_event(&workflow_id, &timer_id, 1),
913 timer_cancelled_event(&workflow_id, &timer_id, 2),
914 timer_started_event(&workflow_id, &timer_id, 3),
915 ];
916 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
917 }
918
919 #[test]
920 fn prior_run_segment_timer_is_not_live() {
921 let workflow_id = workflow_id();
924 let prior = named_timer_id();
925 let current = make_named("current-deadline");
926 let history = vec![
927 workflow_started_event(&workflow_id, 0),
928 timer_started_event(&workflow_id, &prior, 1),
929 workflow_started_event(&workflow_id, 2),
931 timer_started_event(&workflow_id, ¤t, 3),
932 ];
933 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
934 }
935
936 #[test]
941 fn disposition_tracks_the_last_event_for_the_id_and_agrees_with_liveness() {
942 let workflow_id = workflow_id();
943 let timer_id = named_timer_id();
944 let other = make_named("unrelated");
945 let assert_agrees = |history: &[Event], expected: TimerDisposition| {
946 assert_eq!(
947 timer_disposition_in_active_segment(history, &timer_id),
948 expected
949 );
950 assert_eq!(
951 live_timers_in_active_segment(history).contains(&timer_id),
952 expected == TimerDisposition::Live,
953 "the per-id disposition and the enumerating model disagree on liveness"
954 );
955 };
956
957 let mut history = vec![
959 workflow_started_event(&workflow_id, 0),
960 timer_started_event(&workflow_id, &timer_id, 1),
961 ];
962 assert_agrees(&history, TimerDisposition::Live);
963
964 history.push(timer_fired_event(&workflow_id, &timer_id, 2));
966 assert_agrees(&history, TimerDisposition::Fired);
967
968 history.push(timer_fired_event(&workflow_id, &other, 3));
970 assert_agrees(&history, TimerDisposition::Fired);
971
972 history.push(timer_started_event(&workflow_id, &timer_id, 4));
974 assert_agrees(&history, TimerDisposition::Live);
975
976 history.push(timer_cancelled_event(&workflow_id, &timer_id, 5));
978 assert_agrees(&history, TimerDisposition::Cancelled);
979
980 history.push(workflow_started_event(&workflow_id, 6));
983 assert_agrees(&history, TimerDisposition::Absent);
984
985 assert_agrees(&[], TimerDisposition::Absent);
987 }
988
989 #[tokio::test]
990 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
991 let process = WorkflowProcessHandle::new(42);
996 let (store, engine, service) = service();
997 let workflow_id = workflow_id();
998 let timer_id = named_timer_id();
999 let fire_at = instant(110);
1000 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1001 engine.record_workflow_event(
1002 &workflow_id,
1003 timer_started_event(&workflow_id, &timer_id, 1),
1004 )?;
1005 engine
1006 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1007 engine.record_workflow_event(
1008 &workflow_id,
1009 timer_started_event(&workflow_id, &timer_id, 3),
1010 )?;
1011
1012 service
1013 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1014 .await?;
1015
1016 assert_eq!(
1017 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1018 2,
1019 "the re-armed timer fires again, recording a second TimerFired"
1020 );
1021 assert_eq!(engine.delivered_messages()?.len(), 1);
1022 Ok(())
1023 }
1024
1025 #[tokio::test]
1026 async fn schedule_records_timer_row_without_timer_started_event()
1027 -> Result<(), TimerServiceError> {
1028 let (store, _engine, service) = service();
1029 let workflow_id = workflow_id();
1030 let timer_id = timer_id();
1031 let fire_at = instant(10);
1032
1033 service
1034 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1035 .await?;
1036
1037 let expired = store.expired_timers(fire_at).await?;
1038 assert_eq!(expired.len(), 1);
1039 assert_eq!(expired[0].workflow_id, workflow_id);
1040 assert_eq!(expired[0].timer_id, timer_id);
1041 assert_eq!(expired[0].fire_at, fire_at);
1042
1043 assert!(history(&store, &workflow_id).await?.is_empty());
1044 Ok(())
1045 }
1046
1047 #[tokio::test]
1048 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
1049 let process = WorkflowProcessHandle::new(42);
1050 let (_store, engine, service) = service();
1051 let workflow_id = workflow_id();
1052 let timer_id = timer_id();
1053 let fire_at = instant(20);
1054 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1055
1056 service
1057 .schedule(workflow_id, timer_id.clone(), fire_at, 1)
1058 .await?;
1059
1060 assert_eq!(
1061 engine.armed_timers()?,
1062 vec![TimerWheelEntry {
1063 process,
1064 timer_id,
1065 fire_at
1066 }]
1067 );
1068 Ok(())
1069 }
1070
1071 #[tokio::test]
1072 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
1073 let (store, engine, service) = service();
1074 let workflow_id = workflow_id();
1075 let timer_id = timer_id();
1076 let fire_at = instant(30);
1077 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1078
1079 service
1080 .schedule(workflow_id.clone(), timer_id, fire_at, 1)
1081 .await?;
1082
1083 assert!(engine.armed_timers()?.is_empty());
1084 assert!(history(&store, &workflow_id).await?.is_empty());
1085 Ok(())
1086 }
1087
1088 #[tokio::test]
1089 async fn fire_records_timer_fired_then_delivers_mailbox_message()
1090 -> Result<(), TimerServiceError> {
1091 let process = WorkflowProcessHandle::new(42);
1092 let (store, engine, service) = service();
1093 let workflow_id = workflow_id();
1094 let timer_id = timer_id();
1095 let fire_at = instant(40);
1096 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1097 engine.record_workflow_event(
1098 &workflow_id,
1099 timer_started_event(&workflow_id, &timer_id, 1),
1100 )?;
1101
1102 service
1103 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1104 .await?;
1105
1106 assert_eq!(
1107 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1108 1
1109 );
1110 assert_eq!(
1111 engine.delivered_messages()?,
1112 vec![(
1113 process,
1114 DeliveredWorkflowMessage::TimerFired {
1115 timer_id: timer_id.clone(),
1116 fire_at
1117 }
1118 )]
1119 );
1120 assert!(matches!(
1121 engine.operations()?.as_slice(),
1122 [
1123 FakeEngineOperation::EventRecorded {
1124 event: Event::TimerStarted { .. },
1125 ..
1126 },
1127 FakeEngineOperation::EventRecorded {
1128 workflow_id: recorded_workflow_id,
1129 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
1130 },
1131 FakeEngineOperation::Delivered {
1132 process: delivered_process,
1133 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
1134 }
1135 ] if recorded_workflow_id == &workflow_id
1136 && recorded_timer_id == &timer_id
1137 && delivered_process == &process
1138 && delivered_timer_id == &timer_id
1139 ));
1140 Ok(())
1141 }
1142
1143 #[tokio::test]
1144 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
1145 -> Result<(), TimerServiceError> {
1146 let (store, engine, service) = service();
1147 let workflow_id = workflow_id();
1148 let timer_id = timer_id();
1149 let fire_at = instant(50);
1150 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1151 engine.record_workflow_event(
1152 &workflow_id,
1153 timer_started_event(&workflow_id, &timer_id, 1),
1154 )?;
1155
1156 service
1157 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1158 .await?;
1159
1160 assert_eq!(
1161 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1162 1
1163 );
1164 assert!(engine.delivered_messages()?.is_empty());
1165 Ok(())
1166 }
1167
1168 #[tokio::test]
1177 async fn firing_same_timer_twice_records_once_and_redelivers_the_wake()
1178 -> Result<(), TimerServiceError> {
1179 let process = WorkflowProcessHandle::new(42);
1180 let (store, engine, service) = service();
1181 let workflow_id = workflow_id();
1182 let timer_id = timer_id();
1183 let fire_at = instant(60);
1184 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1185 engine.record_workflow_event(
1186 &workflow_id,
1187 timer_started_event(&workflow_id, &timer_id, 1),
1188 )?;
1189
1190 service
1191 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1192 .await?;
1193 service
1199 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1200 .await?;
1201
1202 assert_eq!(
1203 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1204 1,
1205 "the recorded fire must never be appended a second time"
1206 );
1207 assert_eq!(
1208 engine.delivered_messages()?.len(),
1209 2,
1210 "the second fire re-delivers the owed wake instead of silently no-opping"
1211 );
1212 Ok(())
1213 }
1214
1215 #[tokio::test]
1223 async fn already_recorded_fire_delivers_owed_wake_without_second_append()
1224 -> Result<(), TimerServiceError> {
1225 let process = WorkflowProcessHandle::new(42);
1226 let (store, engine, service) = service();
1227 let workflow_id = workflow_id();
1228 let timer_id = timer_id();
1229 let fire_at = instant(140);
1230 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1231 engine.record_workflow_event(
1232 &workflow_id,
1233 timer_started_event(&workflow_id, &timer_id, 1),
1234 )?;
1235 engine
1236 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1237
1238 service
1239 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1240 .await?;
1241
1242 assert_eq!(
1243 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1244 1,
1245 "the already-recorded fire must not be appended again"
1246 );
1247 assert_eq!(
1248 engine.delivered_messages()?,
1249 vec![(
1250 process,
1251 DeliveredWorkflowMessage::TimerFired { timer_id, fire_at }
1252 )],
1253 "the owed mailbox wake must be delivered"
1254 );
1255 Ok(())
1256 }
1257
1258 #[tokio::test]
1265 async fn already_recorded_fire_for_nonresident_workflow_wakes_nothing()
1266 -> Result<(), TimerServiceError> {
1267 let (store, engine, service) = service();
1268 let workflow_id = workflow_id();
1269 let timer_id = timer_id();
1270 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1271 engine.record_workflow_event(
1272 &workflow_id,
1273 timer_started_event(&workflow_id, &timer_id, 1),
1274 )?;
1275 engine
1276 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1277
1278 service
1279 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(150))
1280 .await?;
1281
1282 assert_eq!(
1283 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1284 1,
1285 "a non-resident redelivery must not re-enter the recorder seam"
1286 );
1287 assert!(
1288 engine.delivered_messages()?.is_empty(),
1289 "no wake is attempted for a non-resident workflow"
1290 );
1291 Ok(())
1292 }
1293
1294 #[tokio::test]
1300 async fn already_recorded_fire_after_run_terminal_delivers_no_wake()
1301 -> Result<(), TimerServiceError> {
1302 let process = WorkflowProcessHandle::new(42);
1303 let (store, engine, service) = service();
1304 let workflow_id = workflow_id();
1305 let timer_id = timer_id();
1306 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1307 engine.record_workflow_event(
1308 &workflow_id,
1309 timer_started_event(&workflow_id, &timer_id, 1),
1310 )?;
1311 engine
1312 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
1313 engine.refuse_next_record_as_terminal()?;
1314
1315 service
1316 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(160))
1317 .await?;
1318
1319 assert_eq!(
1320 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1321 1
1322 );
1323 assert!(
1324 engine.delivered_messages()?.is_empty(),
1325 "a post-terminal redelivery must not wake the terminated run"
1326 );
1327 Ok(())
1328 }
1329
1330 #[tokio::test]
1331 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
1332 let process = WorkflowProcessHandle::new(42);
1333 let (store, engine, service) = service();
1334 let workflow_id = workflow_id();
1335 let timer_id = timer_id();
1336 let fire_at = instant(70);
1337 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1338 engine.record_workflow_event(
1339 &workflow_id,
1340 timer_started_event(&workflow_id, &timer_id, 1),
1341 )?;
1342 let cancelled = Event::TimerCancelled {
1343 cause: TimerCancelCause::WorkflowIntent,
1344 envelope: EventEnvelope {
1345 seq: 2,
1346 recorded_at: instant(69),
1347 workflow_id: workflow_id.clone(),
1348 },
1349 timer_id: timer_id.clone(),
1350 };
1351 engine.record_workflow_event(&workflow_id, cancelled)?;
1352
1353 service
1354 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1355 .await?;
1356
1357 let history = history(&store, &workflow_id).await?;
1358 assert_eq!(count_timer_fired(&history, &timer_id), 0);
1359 assert!(engine.delivered_messages()?.is_empty());
1360 Ok(())
1361 }
1362
1363 #[tokio::test]
1364 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
1365 let process = WorkflowProcessHandle::new(42);
1366 let (store, engine, service) = service();
1367 let workflow_id = workflow_id();
1368 let timer_id = timer_id();
1369 let fire_at = instant(80);
1370
1371 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1372 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
1373 engine.record_workflow_event(
1374 &workflow_id,
1375 timer_started_event(&workflow_id, &timer_id, 1),
1376 )?;
1377 service
1378 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1379 .await?;
1380
1381 assert_eq!(
1382 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1383 1
1384 );
1385 assert!(engine.delivered_messages()?.is_empty());
1386 Ok(())
1387 }
1388
1389 #[tokio::test]
1390 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
1391 let process = WorkflowProcessHandle::new(42);
1392 let (store, engine, service) = service();
1393 let workflow_id = workflow_id();
1394 let timer_id = timer_id();
1395 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1396
1397 service
1398 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
1399 .await?;
1400
1401 assert!(history(&store, &workflow_id).await?.is_empty());
1402 assert!(engine.delivered_messages()?.is_empty());
1403 Ok(())
1404 }
1405
1406 struct RecordingDeadlineHandler {
1408 calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
1409 fail: bool,
1410 }
1411
1412 impl RecordingDeadlineHandler {
1413 fn new(fail: bool) -> Self {
1414 Self {
1415 calls: std::sync::Mutex::new(Vec::new()),
1416 fail,
1417 }
1418 }
1419
1420 fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
1421 self.calls
1422 .lock()
1423 .map(|calls| calls.clone())
1424 .map_err(|error| TimerServiceError::Deadline(error.to_string()))
1425 }
1426 }
1427
1428 #[async_trait::async_trait]
1429 impl DeadlineHandler for RecordingDeadlineHandler {
1430 async fn on_deadline_elapsed(
1431 &self,
1432 workflow_id: WorkflowId,
1433 run_id: RunId,
1434 ) -> Result<(), DeadlineHandlerError> {
1435 self.calls
1436 .lock()
1437 .map_err(|error| DeadlineHandlerError(error.to_string()))?
1438 .push((workflow_id, run_id));
1439 if self.fail {
1440 Err(DeadlineHandlerError(
1441 "deliberate handler failure".to_owned(),
1442 ))
1443 } else {
1444 Ok(())
1445 }
1446 }
1447 }
1448
1449 fn service_with_handler(
1450 handler: Arc<dyn DeadlineHandler>,
1451 ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
1452 let concrete_store = Arc::new(InMemoryStore::default());
1453 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1454 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
1455 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1456 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
1457 .with_deadline_handler(handler);
1458 (concrete_store, engine, service)
1459 }
1460
1461 #[tokio::test]
1464 async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
1465 -> Result<(), TimerServiceError> {
1466 let run_id = RunId::new_v4();
1467 let deadline_id = deadline_timer_id(&run_id)
1468 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1469 let handler = Arc::new(RecordingDeadlineHandler::new(false));
1470 let (store, engine, service) = service_with_handler(handler.clone());
1471 let workflow_id = workflow_id();
1472 let fire_at = instant(120);
1473 engine.set_residency(
1474 workflow_id.clone(),
1475 WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
1476 )?;
1477 engine.record_workflow_event(
1478 &workflow_id,
1479 timer_started_event(&workflow_id, &deadline_id, 1),
1480 )?;
1481
1482 service
1483 .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
1484 .await?;
1485
1486 assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
1487 assert_eq!(
1488 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1489 0,
1490 "a deadline fire never records TimerFired"
1491 );
1492 assert!(engine.delivered_messages()?.is_empty());
1493 Ok(())
1494 }
1495
1496 #[tokio::test]
1499 async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
1500 let run_id = RunId::new_v4();
1501 let deadline_id = deadline_timer_id(&run_id)
1502 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1503 let (store, engine, service) = service();
1504 let workflow_id = workflow_id();
1505 engine.record_workflow_event(
1506 &workflow_id,
1507 timer_started_event(&workflow_id, &deadline_id, 1),
1508 )?;
1509
1510 let result = service
1511 .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
1512 .await;
1513
1514 assert!(
1515 matches!(result, Err(TimerServiceError::Deadline(_))),
1516 "unhandled deadline fire must be a typed error, got {result:?}"
1517 );
1518 assert_eq!(
1519 count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
1520 0
1521 );
1522 Ok(())
1523 }
1524
1525 #[tokio::test]
1527 async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1528 let run_id = RunId::new_v4();
1529 let deadline_id = deadline_timer_id(&run_id)
1530 .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1531 let handler = Arc::new(RecordingDeadlineHandler::new(true));
1532 let (_store, engine, service) = service_with_handler(handler);
1533 let workflow_id = workflow_id();
1534 engine.record_workflow_event(
1535 &workflow_id,
1536 timer_started_event(&workflow_id, &deadline_id, 1),
1537 )?;
1538
1539 let result = service
1540 .fire_timer(workflow_id, deadline_id, instant(120))
1541 .await;
1542
1543 assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1544 Ok(())
1545 }
1546
1547 #[tokio::test]
1553 async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1554 -> Result<(), TimerServiceError> {
1555 let process = WorkflowProcessHandle::new(42);
1556 let (store, engine, service) = service();
1557 let workflow_id = workflow_id();
1558 let timer_id = timer_id();
1559 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1560 engine.record_workflow_event(
1561 &workflow_id,
1562 timer_started_event(&workflow_id, &timer_id, 1),
1563 )?;
1564 engine.refuse_next_record_as_terminal()?;
1565
1566 service
1567 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1568 .await?;
1569
1570 assert_eq!(
1571 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1572 0,
1573 "a refused fire records no TimerFired"
1574 );
1575 assert!(
1576 engine.delivered_messages()?.is_empty(),
1577 "a refused fire delivers no wake"
1578 );
1579 Ok(())
1580 }
1581
1582 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1590 async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1591 -> Result<(), TimerServiceError> {
1592 use dashmap::DashSet;
1593 use tokio::sync::Barrier;
1594
1595 for _ in 0..20 {
1596 let process = WorkflowProcessHandle::new(42);
1597 let concrete_store = Arc::new(InMemoryStore::default());
1598 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1599 let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1600 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1601 let coordinator = Arc::new(DashSet::new());
1602 let service_a =
1603 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1604 .with_terminal_updates(Arc::clone(&coordinator));
1605 let service_b =
1606 TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1607 .with_terminal_updates(Arc::clone(&coordinator));
1608
1609 let workflow_id = workflow_id();
1610 let timer_id = timer_id();
1611 let fire_at = instant(200);
1612 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1613 engine.record_workflow_event(
1614 &workflow_id,
1615 timer_started_event(&workflow_id, &timer_id, 1),
1616 )?;
1617
1618 let gate = Arc::new(Barrier::new(2));
1619 let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1620 let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1621 let cancel = async move {
1622 cancel_gate.wait().await;
1623 service_a
1624 .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1625 .await
1626 };
1627 let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1628 let fire = async move {
1629 fire_gate.wait().await;
1630 service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1631 };
1632 let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1633 cancel_result?;
1634 fire_result?;
1635
1636 let history = history(&concrete_store, &workflow_id).await?;
1637 let terminal_timer_events = history
1638 .iter()
1639 .filter(|event| {
1640 matches!(
1641 event,
1642 Event::TimerFired { timer_id: recorded, .. }
1643 | Event::TimerCancelled { timer_id: recorded, .. }
1644 if recorded == &timer_id
1645 )
1646 })
1647 .count();
1648 assert_eq!(
1649 terminal_timer_events, 1,
1650 "first-recorded wins across shared services: {history:#?}"
1651 );
1652 }
1653 Ok(())
1654 }
1655
1656 #[tokio::test]
1657 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1658 {
1659 let process = WorkflowProcessHandle::new(42);
1660 let (store, engine, service) = service();
1661 let workflow_id = workflow_id();
1662 let timer_id = timer_id();
1663 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1664 engine.record_workflow_event(
1666 &workflow_id,
1667 timer_started_event(&workflow_id, &timer_id, 1),
1668 )?;
1669 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1670
1671 service
1672 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1673 .await?;
1674
1675 assert_eq!(
1676 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1677 0
1678 );
1679 assert!(engine.delivered_messages()?.is_empty());
1680 Ok(())
1681 }
1682
1683 async fn outstanding_rows(
1688 store: &InMemoryStore,
1689 as_of: DateTime<Utc>,
1690 ) -> Result<usize, StoreError> {
1691 Ok(store.expired_timers(as_of).await?.len())
1692 }
1693
1694 #[tokio::test]
1698 async fn a_recorded_fire_retires_the_consumed_row() -> Result<(), TimerServiceError> {
1699 let process = WorkflowProcessHandle::new(42);
1700 let (store, engine, service) = service();
1701 let workflow_id = workflow_id();
1702 let timer_id = timer_id();
1703 let fire_at = instant(40);
1704 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1705 engine.record_workflow_event(
1706 &workflow_id,
1707 timer_started_event(&workflow_id, &timer_id, 1),
1708 )?;
1709 service
1710 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1711 .await?;
1712 assert_eq!(
1713 outstanding_rows(&store, instant(1_000)).await?,
1714 1,
1715 "precondition: the arming's row is durable before the fire"
1716 );
1717
1718 service
1719 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1720 .await?;
1721
1722 assert_eq!(
1723 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1724 1,
1725 "the fire itself must still record"
1726 );
1727 assert_eq!(
1728 outstanding_rows(&store, instant(1_000)).await?,
1729 0,
1730 "a recorded fire must retire the consumed arming's row"
1731 );
1732 Ok(())
1733 }
1734
1735 #[tokio::test]
1738 async fn a_recorded_cancel_retires_the_consumed_row() -> Result<(), TimerServiceError> {
1739 let process = WorkflowProcessHandle::new(42);
1740 let (store, engine, service) = service();
1741 let workflow_id = workflow_id();
1742 let timer_id = named_timer_id();
1743 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1744 engine.record_workflow_event(
1748 &workflow_id,
1749 timer_started_event(&workflow_id, &timer_id, 1),
1750 )?;
1751 service
1752 .schedule(workflow_id.clone(), timer_id.clone(), instant(5), 1)
1753 .await?;
1754 assert_eq!(outstanding_rows(&store, instant(1_000)).await?, 1);
1755
1756 service
1757 .cancel(
1758 workflow_id.clone(),
1759 timer_id.clone(),
1760 TimerCancelCause::WorkflowIntent,
1761 )
1762 .await?;
1763
1764 assert_eq!(
1765 outstanding_rows(&store, instant(1_000)).await?,
1766 0,
1767 "a recorded cancel must retire the cancelled arming's row"
1768 );
1769 Ok(())
1770 }
1771
1772 #[tokio::test]
1778 async fn a_stale_fire_leaves_a_re_armed_timers_row() -> Result<(), TimerServiceError> {
1779 let process = WorkflowProcessHandle::new(42);
1780 let (store, engine, service) = service();
1781 let workflow_id = workflow_id();
1782 let timer_id = named_timer_id();
1783 let old_fire_at = instant(5);
1784 let new_fire_at = instant(500);
1785 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1786 engine.record_workflow_event(
1787 &workflow_id,
1788 timer_started_event(&workflow_id, &timer_id, 1),
1789 )?;
1790 service
1792 .schedule(workflow_id.clone(), timer_id.clone(), new_fire_at, 2)
1793 .await?;
1794
1795 service
1797 .fire_timer(workflow_id.clone(), timer_id.clone(), old_fire_at)
1798 .await?;
1799
1800 assert_eq!(
1801 outstanding_rows(&store, instant(1_000)).await?,
1802 1,
1803 "the re-armed row is the replacement arming's only durable claim \
1804 to a recovery fire; a stale retire must leave it standing"
1805 );
1806 Ok(())
1807 }
1808
1809 #[tokio::test]
1813 async fn a_terminal_refused_fire_retires_the_row() -> Result<(), TimerServiceError> {
1814 let process = WorkflowProcessHandle::new(42);
1815 let (store, engine, service) = service();
1816 let workflow_id = workflow_id();
1817 let timer_id = timer_id();
1818 let fire_at = instant(40);
1819 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1820 engine.record_workflow_event(
1821 &workflow_id,
1822 timer_started_event(&workflow_id, &timer_id, 1),
1823 )?;
1824 service
1825 .schedule(workflow_id.clone(), timer_id.clone(), fire_at, 1)
1826 .await?;
1827 engine.refuse_next_record_as_terminal()?;
1828
1829 service
1830 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
1831 .await?;
1832
1833 assert_eq!(
1834 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1835 0,
1836 "the refused fire must record nothing"
1837 );
1838 assert!(
1839 engine.delivered_messages()?.is_empty(),
1840 "a refused fire must wake nothing"
1841 );
1842 assert_eq!(
1843 outstanding_rows(&store, instant(1_000)).await?,
1844 0,
1845 "a terminal-refused fire's arming is moot forever; its row retires"
1846 );
1847 Ok(())
1848 }
1849}