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, 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
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        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    /// Handles a live timer-wheel fire.
157    ///
158    /// `TimerFired` is recorded before any mailbox delivery. If the workflow is no longer resident,
159    /// the recorded event remains the durable observation that replay/recovery can surface later.
160    ///
161    /// # Errors
162    ///
163    /// Returns [`TimerServiceError`] when history inspection, recording, residency resolution, or
164    /// live mailbox delivery fails.
165    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    /// Whether the timer is currently live (started and not since retired) in
223    /// the workflow's active run segment, by last-event-wins.
224    ///
225    /// A timer belongs to the run that recorded its `TimerStarted`, and
226    /// anonymous timer identities are run-scoped ordinals that replacement
227    /// runs (continue-as-new) re-allocate from zero. Scoping the check to
228    /// the latest run segment keeps a stale fire from a finished run from
229    /// recording into — or suppressing — the replacement run's identically
230    /// named timer.
231    ///
232    /// Liveness is decided by the *last* timer event for the id (see
233    /// [`live_timers_in_active_segment`]): a re-armed named timer
234    /// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly live
235    /// again rather than judged terminal forever by the earlier `TimerFired`.
236    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
256/// The live timer ids in the workflow's active run segment, by last-event-wins.
257///
258/// Scans forward from the latest `WorkflowStarted` (the active run segment) and
259/// lets the *last* event for each timer id decide its liveness: a `TimerStarted`
260/// (re)arms it, a `TimerFired`/`TimerCancelled` retires it. This means a *named*
261/// timer that fired or was cancelled and then re-armed within the same segment
262/// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly reported live
263/// again, rather than judged terminal forever by the earlier terminal event.
264///
265/// Start order is preserved and a timer id started more than once is deduped, so
266/// the result is a stable, history-derived (and therefore replay-deterministic)
267/// view of which timers are outstanding. This is the single liveness model shared
268/// by [`TimerService::timer_is_live`] (firing/cancel guard) and the cancel-path
269/// enumerator in `engine::api`, so the two cannot diverge.
270pub(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        // The name is a non-empty literal, so construction never fails; the
401        // anonymous fallback only exists to keep the helper total without an
402        // `unwrap`/`expect` (disallowed by clippy in this crate).
403        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    // --- `live_timers_in_active_segment` / `timer_is_live` semantics ---
411
412    #[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        // The bug fix: a named timer that fired then was re-armed in the same run
450        // segment must be live again (last-event-wins), not judged terminal forever
451        // by the earlier `TimerFired`.
452        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        // A timer started in a run segment that a later `WorkflowStarted` closed
483        // (continue-as-new) is out of scope for the active segment.
484        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            // New run segment begins; the prior timer must not be surfaced.
491            workflow_started_event(&workflow_id, 2),
492            timer_started_event(&workflow_id, &current, 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        // End-to-end firing-path guard: with last-event-wins, a re-armed named
500        // timer is live, so `fire_timer` records a second `TimerFired` and
501        // delivers it — rather than silently no-opping under the old
502        // `any`-semantics.
503        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        // Run 1 started the timer; run 2's WorkflowStarted closes that segment.
789        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}