1use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, TimerCancelCause, TimerId, WorkflowId};
6use aion_store::{ReadableEventStore, StoreError};
7use chrono::{DateTime, Utc};
8use dashmap::DashSet;
9
10use crate::engine_seam::{
11 EngineHandle, EngineSeamError, TimerWheelEntry, WorkflowMailboxMessage, WorkflowResidency,
12};
13
14pub struct TimerService {
20 engine: Arc<dyn EngineHandle>,
21 store: Arc<dyn ReadableEventStore>,
22 recorded_at: fn() -> DateTime<Utc>,
23 terminal_updates: DashSet<(WorkflowId, TimerId)>,
24}
25
26struct TerminalUpdateSlot<'a> {
27 terminal_updates: &'a DashSet<(WorkflowId, TimerId)>,
28 key: (WorkflowId, TimerId),
29}
30
31impl Drop for TerminalUpdateSlot<'_> {
32 fn drop(&mut self) {
33 self.terminal_updates.remove(&self.key);
34 }
35}
36
37#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
39pub enum TimerServiceError {
40 #[error("timer store operation failed: {0}")]
42 Store(#[from] StoreError),
43
44 #[error("timer engine operation failed: {0}")]
46 Engine(#[from] EngineSeamError),
47}
48
49impl TimerService {
50 #[must_use]
52 pub fn new(engine: Arc<dyn EngineHandle>, store: Arc<dyn ReadableEventStore>) -> Self {
53 Self::with_recorded_at(engine, store, Utc::now)
54 }
55
56 #[must_use]
58 pub fn with_recorded_at(
59 engine: Arc<dyn EngineHandle>,
60 store: Arc<dyn ReadableEventStore>,
61 recorded_at: fn() -> DateTime<Utc>,
62 ) -> Self {
63 Self {
64 engine,
65 store,
66 recorded_at,
67 terminal_updates: DashSet::new(),
68 }
69 }
70
71 pub async fn schedule(
82 &self,
83 workflow_id: WorkflowId,
84 timer_id: TimerId,
85 fire_at: DateTime<Utc>,
86 ) -> Result<(), TimerServiceError> {
87 self.store
88 .schedule_timer(&workflow_id, &timer_id, fire_at)
89 .await?;
90
91 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
92 self.engine.arm_timer(TimerWheelEntry {
93 process,
94 timer_id,
95 fire_at,
96 })?;
97 }
98
99 Ok(())
100 }
101
102 pub async fn cancel(
119 &self,
120 workflow_id: WorkflowId,
121 timer_id: TimerId,
122 cause: TimerCancelCause,
123 ) -> Result<(), TimerServiceError> {
124 let key = (workflow_id.clone(), timer_id.clone());
125 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
126
127 let result = self.cancel_guarded(workflow_id, timer_id, cause).await;
128 drop(terminal_update_slot);
129 result
130 }
131
132 async fn cancel_guarded(
133 &self,
134 workflow_id: WorkflowId,
135 timer_id: TimerId,
136 cause: TimerCancelCause,
137 ) -> Result<(), TimerServiceError> {
138 if !self.timer_is_live(&workflow_id, &timer_id).await? {
139 return Ok(());
140 }
141
142 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
143 self.engine.disarm_timer(process, &timer_id)?;
144 }
145
146 let event = Event::TimerCancelled {
147 envelope: self.next_envelope(&workflow_id).await?,
148 timer_id,
149 cause,
150 };
151 self.engine.record_workflow_event(&workflow_id, event)?;
152
153 Ok(())
154 }
155
156 pub async fn fire_timer(
166 &self,
167 workflow_id: WorkflowId,
168 timer_id: TimerId,
169 fire_at: DateTime<Utc>,
170 ) -> Result<(), TimerServiceError> {
171 let key = (workflow_id.clone(), timer_id.clone());
172 let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
173
174 let result = self
175 .fire_timer_guarded(workflow_id, timer_id, fire_at)
176 .await;
177 drop(terminal_update_slot);
178 result
179 }
180
181 async fn wait_for_terminal_update_slot(
182 &self,
183 key: (WorkflowId, TimerId),
184 ) -> TerminalUpdateSlot<'_> {
185 loop {
186 if self.terminal_updates.insert(key.clone()) {
187 return TerminalUpdateSlot {
188 terminal_updates: &self.terminal_updates,
189 key,
190 };
191 }
192 tokio::task::yield_now().await;
193 }
194 }
195
196 async fn fire_timer_guarded(
197 &self,
198 workflow_id: WorkflowId,
199 timer_id: TimerId,
200 fire_at: DateTime<Utc>,
201 ) -> Result<(), TimerServiceError> {
202 if !self.timer_is_live(&workflow_id, &timer_id).await? {
203 return Ok(());
204 }
205
206 let event = Event::TimerFired {
207 envelope: self.next_envelope(&workflow_id).await?,
208 timer_id: timer_id.clone(),
209 };
210 self.engine.record_workflow_event(&workflow_id, event)?;
211
212 if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
213 self.engine.deliver_workflow_message(
214 process,
215 WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
216 )?;
217 }
218
219 Ok(())
220 }
221
222 async fn timer_is_live(
237 &self,
238 workflow_id: &WorkflowId,
239 timer_id: &TimerId,
240 ) -> Result<bool, StoreError> {
241 let history = self.store.read_history(workflow_id).await?;
242 Ok(live_timers_in_active_segment(&history).contains(timer_id))
243 }
244
245 async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
246 let history = self.store.read_history(workflow_id).await?;
247 let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
248 Ok(EventEnvelope {
249 seq,
250 recorded_at: (self.recorded_at)(),
251 workflow_id: workflow_id.clone(),
252 })
253 }
254}
255
256pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
271 let segment_start = history
272 .iter()
273 .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
274 .unwrap_or(0);
275 let mut live: Vec<TimerId> = Vec::new();
276 for event in &history[segment_start..] {
277 match event {
278 Event::TimerStarted { timer_id, .. } => {
279 if !live.contains(timer_id) {
280 live.push(timer_id.clone());
281 }
282 }
283 Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
284 live.retain(|id| id != timer_id);
285 }
286 _ => {}
287 }
288 }
289 live
290}
291
292#[cfg(test)]
293mod tests {
294 use std::sync::Arc;
295
296 use aion_core::{Event, EventEnvelope, TimerCancelCause, TimerId, WorkflowId};
297 use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
298 use chrono::{DateTime, Utc};
299
300 use super::{TimerService, TimerServiceError, live_timers_in_active_segment};
301 use crate::engine_seam::test_support::{
302 DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
303 };
304 use crate::engine_seam::{
305 EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
306 };
307
308 fn instant(offset_seconds: i64) -> DateTime<Utc> {
309 DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
310 }
311
312 fn workflow_id() -> WorkflowId {
313 WorkflowId::new_v4()
314 }
315
316 fn timer_id() -> TimerId {
317 TimerId::anonymous(7)
318 }
319
320 fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
321 let concrete_store = Arc::new(InMemoryStore::default());
322 let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
323 let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
324 let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
325 let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
326 (concrete_store, engine, service)
327 }
328
329 fn recorded_at() -> DateTime<Utc> {
330 instant(1)
331 }
332
333 async fn history(
334 store: &InMemoryStore,
335 workflow_id: &WorkflowId,
336 ) -> Result<Vec<Event>, StoreError> {
337 store.read_history(workflow_id).await
338 }
339
340 fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
341 events
342 .iter()
343 .filter(|event| {
344 matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
345 })
346 .count()
347 }
348
349 fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
350 Event::TimerStarted {
351 envelope: EventEnvelope {
352 seq,
353 recorded_at: instant(0),
354 workflow_id: workflow_id.clone(),
355 },
356 timer_id: timer_id.clone(),
357 fire_at: instant(5),
358 }
359 }
360
361 fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
362 Event::WorkflowStarted {
363 envelope: EventEnvelope {
364 seq,
365 recorded_at: instant(0),
366 workflow_id: workflow_id.clone(),
367 },
368 workflow_type: "fixture".to_owned(),
369 input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
370 run_id: aion_core::RunId::new_v4(),
371 parent_run_id: None,
372 package_version: aion_core::PackageVersion::new("a".repeat(64)),
373 }
374 }
375
376 fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
377 Event::TimerFired {
378 envelope: EventEnvelope {
379 seq,
380 recorded_at: instant(0),
381 workflow_id: workflow_id.clone(),
382 },
383 timer_id: timer_id.clone(),
384 }
385 }
386
387 fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
388 Event::TimerCancelled {
389 cause: TimerCancelCause::WorkflowIntent,
390 envelope: EventEnvelope {
391 seq,
392 recorded_at: instant(0),
393 workflow_id: workflow_id.clone(),
394 },
395 timer_id: timer_id.clone(),
396 }
397 }
398
399 fn make_named(name: &str) -> TimerId {
400 TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
404 }
405
406 fn named_timer_id() -> TimerId {
407 make_named("review-deadline")
408 }
409
410 #[test]
413 fn started_timer_is_live() {
414 let workflow_id = workflow_id();
415 let timer_id = named_timer_id();
416 let history = vec![
417 workflow_started_event(&workflow_id, 0),
418 timer_started_event(&workflow_id, &timer_id, 1),
419 ];
420 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
421 }
422
423 #[test]
424 fn started_then_fired_timer_is_dead() {
425 let workflow_id = workflow_id();
426 let timer_id = named_timer_id();
427 let history = vec![
428 workflow_started_event(&workflow_id, 0),
429 timer_started_event(&workflow_id, &timer_id, 1),
430 timer_fired_event(&workflow_id, &timer_id, 2),
431 ];
432 assert!(live_timers_in_active_segment(&history).is_empty());
433 }
434
435 #[test]
436 fn started_then_cancelled_timer_is_dead() {
437 let workflow_id = workflow_id();
438 let timer_id = named_timer_id();
439 let history = vec![
440 workflow_started_event(&workflow_id, 0),
441 timer_started_event(&workflow_id, &timer_id, 1),
442 timer_cancelled_event(&workflow_id, &timer_id, 2),
443 ];
444 assert!(live_timers_in_active_segment(&history).is_empty());
445 }
446
447 #[test]
448 fn restarted_named_timer_after_fire_is_live() {
449 let workflow_id = workflow_id();
453 let timer_id = named_timer_id();
454 let history = vec![
455 workflow_started_event(&workflow_id, 0),
456 timer_started_event(&workflow_id, &timer_id, 1),
457 timer_fired_event(&workflow_id, &timer_id, 2),
458 timer_started_event(&workflow_id, &timer_id, 3),
459 ];
460 assert_eq!(
461 live_timers_in_active_segment(&history),
462 vec![timer_id],
463 "a re-armed named timer is live again"
464 );
465 }
466
467 #[test]
468 fn restarted_named_timer_after_cancel_is_live() {
469 let workflow_id = workflow_id();
470 let timer_id = named_timer_id();
471 let history = vec![
472 workflow_started_event(&workflow_id, 0),
473 timer_started_event(&workflow_id, &timer_id, 1),
474 timer_cancelled_event(&workflow_id, &timer_id, 2),
475 timer_started_event(&workflow_id, &timer_id, 3),
476 ];
477 assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
478 }
479
480 #[test]
481 fn prior_run_segment_timer_is_not_live() {
482 let workflow_id = workflow_id();
485 let prior = named_timer_id();
486 let current = make_named("current-deadline");
487 let history = vec![
488 workflow_started_event(&workflow_id, 0),
489 timer_started_event(&workflow_id, &prior, 1),
490 workflow_started_event(&workflow_id, 2),
492 timer_started_event(&workflow_id, ¤t, 3),
493 ];
494 assert_eq!(live_timers_in_active_segment(&history), vec![current]);
495 }
496
497 #[tokio::test]
498 async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
499 let process = WorkflowProcessHandle::new(42);
504 let (store, engine, service) = service();
505 let workflow_id = workflow_id();
506 let timer_id = named_timer_id();
507 let fire_at = instant(110);
508 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
509 engine.record_workflow_event(
510 &workflow_id,
511 timer_started_event(&workflow_id, &timer_id, 1),
512 )?;
513 engine
514 .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
515 engine.record_workflow_event(
516 &workflow_id,
517 timer_started_event(&workflow_id, &timer_id, 3),
518 )?;
519
520 service
521 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
522 .await?;
523
524 assert_eq!(
525 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
526 2,
527 "the re-armed timer fires again, recording a second TimerFired"
528 );
529 assert_eq!(engine.delivered_messages()?.len(), 1);
530 Ok(())
531 }
532
533 #[tokio::test]
534 async fn schedule_records_timer_row_without_timer_started_event()
535 -> Result<(), TimerServiceError> {
536 let (store, _engine, service) = service();
537 let workflow_id = workflow_id();
538 let timer_id = timer_id();
539 let fire_at = instant(10);
540
541 service
542 .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
543 .await?;
544
545 let expired = store.expired_timers(fire_at).await?;
546 assert_eq!(expired.len(), 1);
547 assert_eq!(expired[0].workflow_id, workflow_id);
548 assert_eq!(expired[0].timer_id, timer_id);
549 assert_eq!(expired[0].fire_at, fire_at);
550
551 assert!(history(&store, &workflow_id).await?.is_empty());
552 Ok(())
553 }
554
555 #[tokio::test]
556 async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
557 let process = WorkflowProcessHandle::new(42);
558 let (_store, engine, service) = service();
559 let workflow_id = workflow_id();
560 let timer_id = timer_id();
561 let fire_at = instant(20);
562 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
563
564 service
565 .schedule(workflow_id, timer_id.clone(), fire_at)
566 .await?;
567
568 assert_eq!(
569 engine.armed_timers()?,
570 vec![TimerWheelEntry {
571 process,
572 timer_id,
573 fire_at
574 }]
575 );
576 Ok(())
577 }
578
579 #[tokio::test]
580 async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
581 let (store, engine, service) = service();
582 let workflow_id = workflow_id();
583 let timer_id = timer_id();
584 let fire_at = instant(30);
585 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
586
587 service
588 .schedule(workflow_id.clone(), timer_id, fire_at)
589 .await?;
590
591 assert!(engine.armed_timers()?.is_empty());
592 assert!(history(&store, &workflow_id).await?.is_empty());
593 Ok(())
594 }
595
596 #[tokio::test]
597 async fn fire_records_timer_fired_then_delivers_mailbox_message()
598 -> Result<(), TimerServiceError> {
599 let process = WorkflowProcessHandle::new(42);
600 let (store, engine, service) = service();
601 let workflow_id = workflow_id();
602 let timer_id = timer_id();
603 let fire_at = instant(40);
604 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
605 engine.record_workflow_event(
606 &workflow_id,
607 timer_started_event(&workflow_id, &timer_id, 1),
608 )?;
609
610 service
611 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
612 .await?;
613
614 assert_eq!(
615 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
616 1
617 );
618 assert_eq!(
619 engine.delivered_messages()?,
620 vec![(
621 process,
622 DeliveredWorkflowMessage::TimerFired {
623 timer_id: timer_id.clone(),
624 fire_at
625 }
626 )]
627 );
628 assert!(matches!(
629 engine.operations()?.as_slice(),
630 [
631 FakeEngineOperation::EventRecorded {
632 event: Event::TimerStarted { .. },
633 ..
634 },
635 FakeEngineOperation::EventRecorded {
636 workflow_id: recorded_workflow_id,
637 event: Event::TimerFired { timer_id: recorded_timer_id, .. },
638 },
639 FakeEngineOperation::Delivered {
640 process: delivered_process,
641 message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
642 }
643 ] if recorded_workflow_id == &workflow_id
644 && recorded_timer_id == &timer_id
645 && delivered_process == &process
646 && delivered_timer_id == &timer_id
647 ));
648 Ok(())
649 }
650
651 #[tokio::test]
652 async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
653 -> Result<(), TimerServiceError> {
654 let (store, engine, service) = service();
655 let workflow_id = workflow_id();
656 let timer_id = timer_id();
657 let fire_at = instant(50);
658 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
659 engine.record_workflow_event(
660 &workflow_id,
661 timer_started_event(&workflow_id, &timer_id, 1),
662 )?;
663
664 service
665 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
666 .await?;
667
668 assert_eq!(
669 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
670 1
671 );
672 assert!(engine.delivered_messages()?.is_empty());
673 Ok(())
674 }
675
676 #[tokio::test]
677 async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
678 let process = WorkflowProcessHandle::new(42);
679 let (store, engine, service) = service();
680 let workflow_id = workflow_id();
681 let timer_id = timer_id();
682 let fire_at = instant(60);
683 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
684 engine.record_workflow_event(
685 &workflow_id,
686 timer_started_event(&workflow_id, &timer_id, 1),
687 )?;
688
689 service
690 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
691 .await?;
692 service
693 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
694 .await?;
695
696 assert_eq!(
697 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
698 1
699 );
700 assert_eq!(engine.delivered_messages()?.len(), 1);
701 Ok(())
702 }
703
704 #[tokio::test]
705 async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
706 let process = WorkflowProcessHandle::new(42);
707 let (store, engine, service) = service();
708 let workflow_id = workflow_id();
709 let timer_id = timer_id();
710 let fire_at = instant(70);
711 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
712 engine.record_workflow_event(
713 &workflow_id,
714 timer_started_event(&workflow_id, &timer_id, 1),
715 )?;
716 let cancelled = Event::TimerCancelled {
717 cause: TimerCancelCause::WorkflowIntent,
718 envelope: EventEnvelope {
719 seq: 2,
720 recorded_at: instant(69),
721 workflow_id: workflow_id.clone(),
722 },
723 timer_id: timer_id.clone(),
724 };
725 engine.record_workflow_event(&workflow_id, cancelled)?;
726
727 service
728 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
729 .await?;
730
731 let history = history(&store, &workflow_id).await?;
732 assert_eq!(count_timer_fired(&history, &timer_id), 0);
733 assert!(engine.delivered_messages()?.is_empty());
734 Ok(())
735 }
736
737 #[tokio::test]
738 async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
739 let process = WorkflowProcessHandle::new(42);
740 let (store, engine, service) = service();
741 let workflow_id = workflow_id();
742 let timer_id = timer_id();
743 let fire_at = instant(80);
744
745 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
746 engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
747 engine.record_workflow_event(
748 &workflow_id,
749 timer_started_event(&workflow_id, &timer_id, 1),
750 )?;
751 service
752 .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
753 .await?;
754
755 assert_eq!(
756 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
757 1
758 );
759 assert!(engine.delivered_messages()?.is_empty());
760 Ok(())
761 }
762
763 #[tokio::test]
764 async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
765 let process = WorkflowProcessHandle::new(42);
766 let (store, engine, service) = service();
767 let workflow_id = workflow_id();
768 let timer_id = timer_id();
769 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
770
771 service
772 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
773 .await?;
774
775 assert!(history(&store, &workflow_id).await?.is_empty());
776 assert!(engine.delivered_messages()?.is_empty());
777 Ok(())
778 }
779
780 #[tokio::test]
781 async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
782 {
783 let process = WorkflowProcessHandle::new(42);
784 let (store, engine, service) = service();
785 let workflow_id = workflow_id();
786 let timer_id = timer_id();
787 engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
788 engine.record_workflow_event(
790 &workflow_id,
791 timer_started_event(&workflow_id, &timer_id, 1),
792 )?;
793 engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
794
795 service
796 .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
797 .await?;
798
799 assert_eq!(
800 count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
801 0
802 );
803 assert!(engine.delivered_messages()?.is_empty());
804 Ok(())
805 }
806}