Skip to main content

aion/time/
timer_service.rs

1//! Durable timer service: schedule, wheel arm, and `TimerFired` delivery.
2
3use std::sync::Arc;
4
5use aion_core::{Event, EventEnvelope, 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
14/// Durable timer scheduling and wheel-fire handling.
15///
16/// The service owns the AT live path for timers. Workflow-issued `TimerStarted` events are recorded
17/// by AD's resume-live handoff before this service is called; this service persists only the durable
18/// timer row and later asynchronous arrival/cancellation history through the engine recorder seam.
19pub 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/// Errors returned by [`TimerService`].
38#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
39pub enum TimerServiceError {
40    /// Durable timer storage or history inspection failed.
41    #[error("timer store operation failed: {0}")]
42    Store(#[from] StoreError),
43
44    /// Engine seam operation failed.
45    #[error("timer engine operation failed: {0}")]
46    Engine(#[from] EngineSeamError),
47}
48
49impl TimerService {
50    /// Creates a durable timer service from the engine seam and timer store.
51    #[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    /// Creates a durable timer service with an injected history timestamp source.
57    #[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    /// Schedules a durable timer and arms the live wheel when the workflow is resident.
72    ///
73    /// The operation persists the durable timer row and arms the wheel when needed. The
74    /// command-issued `TimerStarted` recorder event is appended by AD's resume-live handoff before
75    /// AE/AT reaches this service, so this method deliberately does not record it again.
76    ///
77    /// # Errors
78    ///
79    /// Returns [`TimerServiceError`] when durable storage, recording, residency resolution, or wheel
80    /// arming fails.
81    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    /// Cancels a durable timer that has not already reached a terminal timer state.
103    ///
104    /// Already-fired and already-cancelled timers are treated as idempotent no-ops. For active
105    /// resident timers the live wheel is disarmed through the engine seam before `TimerCancelled` is
106    /// recorded through the workflow recorder seam. Non-resident timers still record the cancellation
107    /// so recovery/replay can suppress a later fire.
108    ///
109    /// Anonymous timers are accepted: authors can never address one (the SDK's `cancel_timer`
110    /// takes a `TimerRef` minted by `start_timer`, which is always named), but the engine settles
111    /// `with_timeout` scope deadlines — anonymous by construction — through this first-recorded-wins
112    /// race against [`Self::fire_timer`].
113    ///
114    /// # Errors
115    ///
116    /// Returns [`TimerServiceError`] when history inspection, residency resolution, wheel disarming,
117    /// or event recording fails.
118    pub async fn cancel(
119        &self,
120        workflow_id: WorkflowId,
121        timer_id: TimerId,
122    ) -> Result<(), TimerServiceError> {
123        let key = (workflow_id.clone(), timer_id.clone());
124        let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
125
126        let result = self.cancel_guarded(workflow_id, timer_id).await;
127        drop(terminal_update_slot);
128        result
129    }
130
131    async fn cancel_guarded(
132        &self,
133        workflow_id: WorkflowId,
134        timer_id: TimerId,
135    ) -> Result<(), TimerServiceError> {
136        if !self.timer_is_live(&workflow_id, &timer_id).await? {
137            return Ok(());
138        }
139
140        if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
141            self.engine.disarm_timer(process, &timer_id)?;
142        }
143
144        let event = Event::TimerCancelled {
145            envelope: self.next_envelope(&workflow_id).await?,
146            timer_id,
147        };
148        self.engine.record_workflow_event(&workflow_id, event)?;
149
150        Ok(())
151    }
152
153    /// Handles a live timer-wheel fire.
154    ///
155    /// `TimerFired` is recorded before any mailbox delivery. If the workflow is no longer resident,
156    /// the recorded event remains the durable observation that replay/recovery can surface later.
157    ///
158    /// # Errors
159    ///
160    /// Returns [`TimerServiceError`] when history inspection, recording, residency resolution, or
161    /// live mailbox delivery fails.
162    pub async fn fire_timer(
163        &self,
164        workflow_id: WorkflowId,
165        timer_id: TimerId,
166        fire_at: DateTime<Utc>,
167    ) -> Result<(), TimerServiceError> {
168        let key = (workflow_id.clone(), timer_id.clone());
169        let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
170
171        let result = self
172            .fire_timer_guarded(workflow_id, timer_id, fire_at)
173            .await;
174        drop(terminal_update_slot);
175        result
176    }
177
178    async fn wait_for_terminal_update_slot(
179        &self,
180        key: (WorkflowId, TimerId),
181    ) -> TerminalUpdateSlot<'_> {
182        loop {
183            if self.terminal_updates.insert(key.clone()) {
184                return TerminalUpdateSlot {
185                    terminal_updates: &self.terminal_updates,
186                    key,
187                };
188            }
189            tokio::task::yield_now().await;
190        }
191    }
192
193    async fn fire_timer_guarded(
194        &self,
195        workflow_id: WorkflowId,
196        timer_id: TimerId,
197        fire_at: DateTime<Utc>,
198    ) -> Result<(), TimerServiceError> {
199        if !self.timer_is_live(&workflow_id, &timer_id).await? {
200            return Ok(());
201        }
202
203        let event = Event::TimerFired {
204            envelope: self.next_envelope(&workflow_id).await?,
205            timer_id: timer_id.clone(),
206        };
207        self.engine.record_workflow_event(&workflow_id, event)?;
208
209        if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
210            self.engine.deliver_workflow_message(
211                process,
212                WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
213            )?;
214        }
215
216        Ok(())
217    }
218
219    /// Whether the timer is currently live (started and not since retired) in
220    /// the workflow's active run segment, by last-event-wins.
221    ///
222    /// A timer belongs to the run that recorded its `TimerStarted`, and
223    /// anonymous timer identities are run-scoped ordinals that replacement
224    /// runs (continue-as-new) re-allocate from zero. Scoping the check to
225    /// the latest run segment keeps a stale fire from a finished run from
226    /// recording into — or suppressing — the replacement run's identically
227    /// named timer.
228    ///
229    /// Liveness is decided by the *last* timer event for the id (see
230    /// [`live_timers_in_active_segment`]): a re-armed named timer
231    /// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly live
232    /// again rather than judged terminal forever by the earlier `TimerFired`.
233    async fn timer_is_live(
234        &self,
235        workflow_id: &WorkflowId,
236        timer_id: &TimerId,
237    ) -> Result<bool, StoreError> {
238        let history = self.store.read_history(workflow_id).await?;
239        Ok(live_timers_in_active_segment(&history).contains(timer_id))
240    }
241
242    async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
243        let history = self.store.read_history(workflow_id).await?;
244        let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
245        Ok(EventEnvelope {
246            seq,
247            recorded_at: (self.recorded_at)(),
248            workflow_id: workflow_id.clone(),
249        })
250    }
251}
252
253/// The live timer ids in the workflow's active run segment, by last-event-wins.
254///
255/// Scans forward from the latest `WorkflowStarted` (the active run segment) and
256/// lets the *last* event for each timer id decide its liveness: a `TimerStarted`
257/// (re)arms it, a `TimerFired`/`TimerCancelled` retires it. This means a *named*
258/// timer that fired or was cancelled and then re-armed within the same segment
259/// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly reported live
260/// again, rather than judged terminal forever by the earlier terminal event.
261///
262/// Start order is preserved and a timer id started more than once is deduped, so
263/// the result is a stable, history-derived (and therefore replay-deterministic)
264/// view of which timers are outstanding. This is the single liveness model shared
265/// by [`TimerService::timer_is_live`] (firing/cancel guard) and the cancel-path
266/// enumerator in `engine::api`, so the two cannot diverge.
267pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
268    let segment_start = history
269        .iter()
270        .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
271        .unwrap_or(0);
272    let mut live: Vec<TimerId> = Vec::new();
273    for event in &history[segment_start..] {
274        match event {
275            Event::TimerStarted { timer_id, .. } => {
276                if !live.contains(timer_id) {
277                    live.push(timer_id.clone());
278                }
279            }
280            Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
281                live.retain(|id| id != timer_id);
282            }
283            _ => {}
284        }
285    }
286    live
287}
288
289#[cfg(test)]
290mod tests {
291    use std::sync::Arc;
292
293    use aion_core::{Event, EventEnvelope, TimerId, WorkflowId};
294    use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
295    use chrono::{DateTime, Utc};
296
297    use super::{TimerService, TimerServiceError, live_timers_in_active_segment};
298    use crate::engine_seam::test_support::{
299        DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
300    };
301    use crate::engine_seam::{
302        EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
303    };
304
305    fn instant(offset_seconds: i64) -> DateTime<Utc> {
306        DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
307    }
308
309    fn workflow_id() -> WorkflowId {
310        WorkflowId::new_v4()
311    }
312
313    fn timer_id() -> TimerId {
314        TimerId::anonymous(7)
315    }
316
317    fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
318        let concrete_store = Arc::new(InMemoryStore::default());
319        let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
320        let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
321        let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
322        let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
323        (concrete_store, engine, service)
324    }
325
326    fn recorded_at() -> DateTime<Utc> {
327        instant(1)
328    }
329
330    async fn history(
331        store: &InMemoryStore,
332        workflow_id: &WorkflowId,
333    ) -> Result<Vec<Event>, StoreError> {
334        store.read_history(workflow_id).await
335    }
336
337    fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
338        events
339            .iter()
340            .filter(|event| {
341                matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
342            })
343            .count()
344    }
345
346    fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
347        Event::TimerStarted {
348            envelope: EventEnvelope {
349                seq,
350                recorded_at: instant(0),
351                workflow_id: workflow_id.clone(),
352            },
353            timer_id: timer_id.clone(),
354            fire_at: instant(5),
355        }
356    }
357
358    fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
359        Event::WorkflowStarted {
360            envelope: EventEnvelope {
361                seq,
362                recorded_at: instant(0),
363                workflow_id: workflow_id.clone(),
364            },
365            workflow_type: "fixture".to_owned(),
366            input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
367            run_id: aion_core::RunId::new_v4(),
368            parent_run_id: None,
369            package_version: aion_core::PackageVersion::new("a".repeat(64)),
370        }
371    }
372
373    fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
374        Event::TimerFired {
375            envelope: EventEnvelope {
376                seq,
377                recorded_at: instant(0),
378                workflow_id: workflow_id.clone(),
379            },
380            timer_id: timer_id.clone(),
381        }
382    }
383
384    fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
385        Event::TimerCancelled {
386            envelope: EventEnvelope {
387                seq,
388                recorded_at: instant(0),
389                workflow_id: workflow_id.clone(),
390            },
391            timer_id: timer_id.clone(),
392        }
393    }
394
395    fn make_named(name: &str) -> TimerId {
396        // The name is a non-empty literal, so construction never fails; the
397        // anonymous fallback only exists to keep the helper total without an
398        // `unwrap`/`expect` (disallowed by clippy in this crate).
399        TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
400    }
401
402    fn named_timer_id() -> TimerId {
403        make_named("review-deadline")
404    }
405
406    // --- `live_timers_in_active_segment` / `timer_is_live` semantics ---
407
408    #[test]
409    fn started_timer_is_live() {
410        let workflow_id = workflow_id();
411        let timer_id = named_timer_id();
412        let history = vec![
413            workflow_started_event(&workflow_id, 0),
414            timer_started_event(&workflow_id, &timer_id, 1),
415        ];
416        assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
417    }
418
419    #[test]
420    fn started_then_fired_timer_is_dead() {
421        let workflow_id = workflow_id();
422        let timer_id = named_timer_id();
423        let history = vec![
424            workflow_started_event(&workflow_id, 0),
425            timer_started_event(&workflow_id, &timer_id, 1),
426            timer_fired_event(&workflow_id, &timer_id, 2),
427        ];
428        assert!(live_timers_in_active_segment(&history).is_empty());
429    }
430
431    #[test]
432    fn started_then_cancelled_timer_is_dead() {
433        let workflow_id = workflow_id();
434        let timer_id = named_timer_id();
435        let history = vec![
436            workflow_started_event(&workflow_id, 0),
437            timer_started_event(&workflow_id, &timer_id, 1),
438            timer_cancelled_event(&workflow_id, &timer_id, 2),
439        ];
440        assert!(live_timers_in_active_segment(&history).is_empty());
441    }
442
443    #[test]
444    fn restarted_named_timer_after_fire_is_live() {
445        // The bug fix: a named timer that fired then was re-armed in the same run
446        // segment must be live again (last-event-wins), not judged terminal forever
447        // by the earlier `TimerFired`.
448        let workflow_id = workflow_id();
449        let timer_id = named_timer_id();
450        let history = vec![
451            workflow_started_event(&workflow_id, 0),
452            timer_started_event(&workflow_id, &timer_id, 1),
453            timer_fired_event(&workflow_id, &timer_id, 2),
454            timer_started_event(&workflow_id, &timer_id, 3),
455        ];
456        assert_eq!(
457            live_timers_in_active_segment(&history),
458            vec![timer_id],
459            "a re-armed named timer is live again"
460        );
461    }
462
463    #[test]
464    fn restarted_named_timer_after_cancel_is_live() {
465        let workflow_id = workflow_id();
466        let timer_id = named_timer_id();
467        let history = vec![
468            workflow_started_event(&workflow_id, 0),
469            timer_started_event(&workflow_id, &timer_id, 1),
470            timer_cancelled_event(&workflow_id, &timer_id, 2),
471            timer_started_event(&workflow_id, &timer_id, 3),
472        ];
473        assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
474    }
475
476    #[test]
477    fn prior_run_segment_timer_is_not_live() {
478        // A timer started in a run segment that a later `WorkflowStarted` closed
479        // (continue-as-new) is out of scope for the active segment.
480        let workflow_id = workflow_id();
481        let prior = named_timer_id();
482        let current = make_named("current-deadline");
483        let history = vec![
484            workflow_started_event(&workflow_id, 0),
485            timer_started_event(&workflow_id, &prior, 1),
486            // New run segment begins; the prior timer must not be surfaced.
487            workflow_started_event(&workflow_id, 2),
488            timer_started_event(&workflow_id, &current, 3),
489        ];
490        assert_eq!(live_timers_in_active_segment(&history), vec![current]);
491    }
492
493    #[tokio::test]
494    async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
495        // End-to-end firing-path guard: with last-event-wins, a re-armed named
496        // timer is live, so `fire_timer` records a second `TimerFired` and
497        // delivers it — rather than silently no-opping under the old
498        // `any`-semantics.
499        let process = WorkflowProcessHandle::new(42);
500        let (store, engine, service) = service();
501        let workflow_id = workflow_id();
502        let timer_id = named_timer_id();
503        let fire_at = instant(110);
504        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
505        engine.record_workflow_event(
506            &workflow_id,
507            timer_started_event(&workflow_id, &timer_id, 1),
508        )?;
509        engine
510            .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
511        engine.record_workflow_event(
512            &workflow_id,
513            timer_started_event(&workflow_id, &timer_id, 3),
514        )?;
515
516        service
517            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
518            .await?;
519
520        assert_eq!(
521            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
522            2,
523            "the re-armed timer fires again, recording a second TimerFired"
524        );
525        assert_eq!(engine.delivered_messages()?.len(), 1);
526        Ok(())
527    }
528
529    #[tokio::test]
530    async fn schedule_records_timer_row_without_timer_started_event()
531    -> Result<(), TimerServiceError> {
532        let (store, _engine, service) = service();
533        let workflow_id = workflow_id();
534        let timer_id = timer_id();
535        let fire_at = instant(10);
536
537        service
538            .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
539            .await?;
540
541        let expired = store.expired_timers(fire_at).await?;
542        assert_eq!(expired.len(), 1);
543        assert_eq!(expired[0].workflow_id, workflow_id);
544        assert_eq!(expired[0].timer_id, timer_id);
545        assert_eq!(expired[0].fire_at, fire_at);
546
547        assert!(history(&store, &workflow_id).await?.is_empty());
548        Ok(())
549    }
550
551    #[tokio::test]
552    async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
553        let process = WorkflowProcessHandle::new(42);
554        let (_store, engine, service) = service();
555        let workflow_id = workflow_id();
556        let timer_id = timer_id();
557        let fire_at = instant(20);
558        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
559
560        service
561            .schedule(workflow_id, timer_id.clone(), fire_at)
562            .await?;
563
564        assert_eq!(
565            engine.armed_timers()?,
566            vec![TimerWheelEntry {
567                process,
568                timer_id,
569                fire_at
570            }]
571        );
572        Ok(())
573    }
574
575    #[tokio::test]
576    async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
577        let (store, engine, service) = service();
578        let workflow_id = workflow_id();
579        let timer_id = timer_id();
580        let fire_at = instant(30);
581        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
582
583        service
584            .schedule(workflow_id.clone(), timer_id, fire_at)
585            .await?;
586
587        assert!(engine.armed_timers()?.is_empty());
588        assert!(history(&store, &workflow_id).await?.is_empty());
589        Ok(())
590    }
591
592    #[tokio::test]
593    async fn fire_records_timer_fired_then_delivers_mailbox_message()
594    -> Result<(), TimerServiceError> {
595        let process = WorkflowProcessHandle::new(42);
596        let (store, engine, service) = service();
597        let workflow_id = workflow_id();
598        let timer_id = timer_id();
599        let fire_at = instant(40);
600        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
601        engine.record_workflow_event(
602            &workflow_id,
603            timer_started_event(&workflow_id, &timer_id, 1),
604        )?;
605
606        service
607            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
608            .await?;
609
610        assert_eq!(
611            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
612            1
613        );
614        assert_eq!(
615            engine.delivered_messages()?,
616            vec![(
617                process,
618                DeliveredWorkflowMessage::TimerFired {
619                    timer_id: timer_id.clone(),
620                    fire_at
621                }
622            )]
623        );
624        assert!(matches!(
625            engine.operations()?.as_slice(),
626            [
627                FakeEngineOperation::EventRecorded {
628                    event: Event::TimerStarted { .. },
629                    ..
630                },
631                FakeEngineOperation::EventRecorded {
632                    workflow_id: recorded_workflow_id,
633                    event: Event::TimerFired { timer_id: recorded_timer_id, .. },
634                },
635                FakeEngineOperation::Delivered {
636                    process: delivered_process,
637                    message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
638                }
639            ] if recorded_workflow_id == &workflow_id
640                && recorded_timer_id == &timer_id
641                && delivered_process == &process
642                && delivered_timer_id == &timer_id
643        ));
644        Ok(())
645    }
646
647    #[tokio::test]
648    async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
649    -> Result<(), TimerServiceError> {
650        let (store, engine, service) = service();
651        let workflow_id = workflow_id();
652        let timer_id = timer_id();
653        let fire_at = instant(50);
654        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
655        engine.record_workflow_event(
656            &workflow_id,
657            timer_started_event(&workflow_id, &timer_id, 1),
658        )?;
659
660        service
661            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
662            .await?;
663
664        assert_eq!(
665            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
666            1
667        );
668        assert!(engine.delivered_messages()?.is_empty());
669        Ok(())
670    }
671
672    #[tokio::test]
673    async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
674        let process = WorkflowProcessHandle::new(42);
675        let (store, engine, service) = service();
676        let workflow_id = workflow_id();
677        let timer_id = timer_id();
678        let fire_at = instant(60);
679        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
680        engine.record_workflow_event(
681            &workflow_id,
682            timer_started_event(&workflow_id, &timer_id, 1),
683        )?;
684
685        service
686            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
687            .await?;
688        service
689            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
690            .await?;
691
692        assert_eq!(
693            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
694            1
695        );
696        assert_eq!(engine.delivered_messages()?.len(), 1);
697        Ok(())
698    }
699
700    #[tokio::test]
701    async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
702        let process = WorkflowProcessHandle::new(42);
703        let (store, engine, service) = service();
704        let workflow_id = workflow_id();
705        let timer_id = timer_id();
706        let fire_at = instant(70);
707        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
708        engine.record_workflow_event(
709            &workflow_id,
710            timer_started_event(&workflow_id, &timer_id, 1),
711        )?;
712        let cancelled = Event::TimerCancelled {
713            envelope: EventEnvelope {
714                seq: 2,
715                recorded_at: instant(69),
716                workflow_id: workflow_id.clone(),
717            },
718            timer_id: timer_id.clone(),
719        };
720        engine.record_workflow_event(&workflow_id, cancelled)?;
721
722        service
723            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
724            .await?;
725
726        let history = history(&store, &workflow_id).await?;
727        assert_eq!(count_timer_fired(&history, &timer_id), 0);
728        assert!(engine.delivered_messages()?.is_empty());
729        Ok(())
730    }
731
732    #[tokio::test]
733    async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
734        let process = WorkflowProcessHandle::new(42);
735        let (store, engine, service) = service();
736        let workflow_id = workflow_id();
737        let timer_id = timer_id();
738        let fire_at = instant(80);
739
740        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
741        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
742        engine.record_workflow_event(
743            &workflow_id,
744            timer_started_event(&workflow_id, &timer_id, 1),
745        )?;
746        service
747            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
748            .await?;
749
750        assert_eq!(
751            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
752            1
753        );
754        assert!(engine.delivered_messages()?.is_empty());
755        Ok(())
756    }
757
758    #[tokio::test]
759    async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
760        let process = WorkflowProcessHandle::new(42);
761        let (store, engine, service) = service();
762        let workflow_id = workflow_id();
763        let timer_id = timer_id();
764        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
765
766        service
767            .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
768            .await?;
769
770        assert!(history(&store, &workflow_id).await?.is_empty());
771        assert!(engine.delivered_messages()?.is_empty());
772        Ok(())
773    }
774
775    #[tokio::test]
776    async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
777    {
778        let process = WorkflowProcessHandle::new(42);
779        let (store, engine, service) = service();
780        let workflow_id = workflow_id();
781        let timer_id = timer_id();
782        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
783        // Run 1 started the timer; run 2's WorkflowStarted closes that segment.
784        engine.record_workflow_event(
785            &workflow_id,
786            timer_started_event(&workflow_id, &timer_id, 1),
787        )?;
788        engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
789
790        service
791            .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
792            .await?;
793
794        assert_eq!(
795            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
796            0
797        );
798        assert!(engine.delivered_messages()?.is_empty());
799        Ok(())
800    }
801}