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, RecordOutcome, TimerWheelEntry, WorkflowMailboxMessage,
12    WorkflowResidency,
13};
14use crate::time::deadline::{DeadlineHandler, deadline_run_id, is_deadline_timer};
15
16/// Durable timer scheduling and wheel-fire handling.
17///
18/// The service owns the AT live path for timers. Workflow-issued `TimerStarted` events are recorded
19/// by AD's resume-live handoff before this service is called; this service persists only the durable
20/// timer row and later asynchronous arrival/cancellation history through the engine recorder seam.
21pub struct TimerService {
22    engine: Arc<dyn EngineHandle>,
23    store: Arc<dyn ReadableEventStore>,
24    recorded_at: fn() -> DateTime<Utc>,
25    /// Per-timer first-recorded-wins coordinator shared across EVERY service
26    /// instance the production bridge hands out. Cancel and fire obtain
27    /// SEPARATE service instances (the live wheel constructs one, `Engine::cancel`
28    /// another), so a per-instance set would not exclude them; a shared `Arc`
29    /// makes a cancel and a fire for the same timer mutually exclude — the
30    /// #cancel-vs-fire race the review flagged. Bare unit-test services get their
31    /// own set, which is correct for a single-instance test.
32    terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
33    /// Engine-registered handler for reserved `deadline:{run_id}` fires.
34    ///
35    /// `None` on a bare service (unit tests): a deadline fire is then a typed
36    /// error, never a silent generic `TimerFired`. The production bridge sets it
37    /// via [`Self::with_deadline_handler`] when constructing the service.
38    deadline_handler: Option<Arc<dyn DeadlineHandler>>,
39}
40
41struct TerminalUpdateSlot<'a> {
42    terminal_updates: &'a DashSet<(WorkflowId, TimerId)>,
43    key: (WorkflowId, TimerId),
44}
45
46impl Drop for TerminalUpdateSlot<'_> {
47    fn drop(&mut self) {
48        self.terminal_updates.remove(&self.key);
49    }
50}
51
52/// Errors returned by [`TimerService`].
53#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
54pub enum TimerServiceError {
55    /// Durable timer storage or history inspection failed.
56    #[error("timer store operation failed: {0}")]
57    Store(#[from] StoreError),
58
59    /// Engine seam operation failed.
60    #[error("timer engine operation failed: {0}")]
61    Engine(#[from] EngineSeamError),
62
63    /// A reserved `deadline:{run_id}` timer fired but could not be routed to a
64    /// registered deadline handler (or the handler failed).
65    ///
66    /// Never a silent generic fire: a deadline timer that reaches
67    /// [`TimerService::fire_timer`] without a handler — or whose handler errors —
68    /// surfaces here so the caller (live wheel or `recover_due`) observes the
69    /// failure rather than recording a spurious `TimerFired`.
70    #[error("deadline timer routing failed: {0}")]
71    Deadline(String),
72}
73
74impl TimerService {
75    /// Creates a durable timer service from the engine seam and timer store.
76    #[must_use]
77    pub fn new(engine: Arc<dyn EngineHandle>, store: Arc<dyn ReadableEventStore>) -> Self {
78        Self::with_recorded_at(engine, store, Utc::now)
79    }
80
81    /// Creates a durable timer service with an injected history timestamp source.
82    #[must_use]
83    pub fn with_recorded_at(
84        engine: Arc<dyn EngineHandle>,
85        store: Arc<dyn ReadableEventStore>,
86        recorded_at: fn() -> DateTime<Utc>,
87    ) -> Self {
88        Self {
89            engine,
90            store,
91            recorded_at,
92            terminal_updates: Arc::new(DashSet::new()),
93            deadline_handler: None,
94        }
95    }
96
97    /// Replaces this service's per-timer terminal-update coordinator with a
98    /// shared one, returning the service for chaining.
99    ///
100    /// The production timer bridge owns ONE coordinator and hands it to every
101    /// [`TimerService`] it constructs, so a cancel obtained from one service and
102    /// a fire obtained from another still serialize per timer (first-recorded
103    /// wins). Without this, each service would guard against itself only.
104    #[must_use]
105    pub fn with_terminal_updates(
106        mut self,
107        terminal_updates: Arc<DashSet<(WorkflowId, TimerId)>>,
108    ) -> Self {
109        self.terminal_updates = terminal_updates;
110        self
111    }
112
113    /// Registers the engine-side deadline handler for reserved `deadline:{run_id}`
114    /// fires, returning the service for chaining.
115    ///
116    /// The production timer bridge calls this so both the live wheel and
117    /// `recover_due` (which share [`Self::fire_timer`]) demux a deadline fire to
118    /// the handler instead of recording a generic `TimerFired`.
119    #[must_use]
120    pub fn with_deadline_handler(mut self, handler: Arc<dyn DeadlineHandler>) -> Self {
121        self.deadline_handler = Some(handler);
122        self
123    }
124
125    /// Schedules a durable timer and arms the live wheel when the workflow is resident.
126    ///
127    /// The operation persists the durable timer row and arms the wheel when needed. The
128    /// command-issued `TimerStarted` recorder event is appended by AD's resume-live handoff before
129    /// AE/AT reaches this service, so this method deliberately does not record it again.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`TimerServiceError`] when durable storage, recording, residency resolution, or wheel
134    /// arming fails.
135    pub async fn schedule(
136        &self,
137        workflow_id: WorkflowId,
138        timer_id: TimerId,
139        fire_at: DateTime<Utc>,
140    ) -> Result<(), TimerServiceError> {
141        self.store
142            .schedule_timer(&workflow_id, &timer_id, fire_at)
143            .await?;
144
145        if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
146            self.engine.arm_timer(TimerWheelEntry {
147                process,
148                timer_id,
149                fire_at,
150            })?;
151        }
152
153        Ok(())
154    }
155
156    /// Cancels a durable timer that has not already reached a terminal timer state.
157    ///
158    /// Already-fired and already-cancelled timers are treated as idempotent no-ops. For active
159    /// resident timers the live wheel is disarmed through the engine seam before `TimerCancelled` is
160    /// recorded through the workflow recorder seam. Non-resident timers still record the cancellation
161    /// so recovery/replay can suppress a later fire.
162    ///
163    /// Anonymous timers are accepted: authors can never address one (the SDK's `cancel_timer`
164    /// takes a `TimerRef` minted by `start_timer`, which is always named), but the engine settles
165    /// `with_timeout` scope deadlines — anonymous by construction — through this first-recorded-wins
166    /// race against [`Self::fire_timer`].
167    ///
168    /// # Errors
169    ///
170    /// Returns [`TimerServiceError`] when history inspection, residency resolution, wheel disarming,
171    /// or event recording fails.
172    pub async fn cancel(
173        &self,
174        workflow_id: WorkflowId,
175        timer_id: TimerId,
176        cause: TimerCancelCause,
177    ) -> Result<(), TimerServiceError> {
178        let key = (workflow_id.clone(), timer_id.clone());
179        let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
180
181        let result = self.cancel_guarded(workflow_id, timer_id, cause).await;
182        drop(terminal_update_slot);
183        result
184    }
185
186    async fn cancel_guarded(
187        &self,
188        workflow_id: WorkflowId,
189        timer_id: TimerId,
190        cause: TimerCancelCause,
191    ) -> Result<(), TimerServiceError> {
192        if !self.timer_is_live(&workflow_id, &timer_id).await? {
193            return Ok(());
194        }
195
196        if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
197            self.engine.disarm_timer(process, &timer_id)?;
198        }
199
200        let event = Event::TimerCancelled {
201            envelope: self.next_envelope(&workflow_id).await?,
202            timer_id,
203            cause,
204        };
205        self.engine.record_workflow_event(&workflow_id, event)?;
206
207        Ok(())
208    }
209
210    /// Handles a live timer-wheel fire.
211    ///
212    /// `TimerFired` is recorded before any mailbox delivery. If the workflow is no longer resident,
213    /// the recorded event remains the durable observation that replay/recovery can surface later.
214    ///
215    /// # Errors
216    ///
217    /// Returns [`TimerServiceError`] when history inspection, recording, residency resolution, or
218    /// live mailbox delivery fails.
219    pub async fn fire_timer(
220        &self,
221        workflow_id: WorkflowId,
222        timer_id: TimerId,
223        fire_at: DateTime<Utc>,
224    ) -> Result<(), TimerServiceError> {
225        let key = (workflow_id.clone(), timer_id.clone());
226        let terminal_update_slot = self.wait_for_terminal_update_slot(key).await;
227
228        let result = self
229            .fire_timer_guarded(workflow_id, timer_id, fire_at)
230            .await;
231        drop(terminal_update_slot);
232        result
233    }
234
235    async fn wait_for_terminal_update_slot(
236        &self,
237        key: (WorkflowId, TimerId),
238    ) -> TerminalUpdateSlot<'_> {
239        loop {
240            if self.terminal_updates.insert(key.clone()) {
241                return TerminalUpdateSlot {
242                    terminal_updates: self.terminal_updates.as_ref(),
243                    key,
244                };
245            }
246            tokio::task::yield_now().await;
247        }
248    }
249
250    async fn fire_timer_guarded(
251        &self,
252        workflow_id: WorkflowId,
253        timer_id: TimerId,
254        fire_at: DateTime<Utc>,
255    ) -> Result<(), TimerServiceError> {
256        if !self.timer_is_live(&workflow_id, &timer_id).await? {
257            return Ok(());
258        }
259
260        // Demux a reserved workflow-deadline timer out of the generic
261        // record-then-deliver path (both the live wheel and `recover_due` reach
262        // here): it never records a `TimerFired` — the registered handler records
263        // `WorkflowTimedOut` and tears the run down instead.
264        if is_deadline_timer(&timer_id) {
265            return self.fire_deadline(workflow_id, timer_id).await;
266        }
267
268        let event = Event::TimerFired {
269            envelope: self.next_envelope(&workflow_id).await?,
270            timer_id: timer_id.clone(),
271        };
272        // Deliver the mailbox wake ONLY for a genuine append. The recorder seam
273        // refuses a late fire that lands after the run terminated
274        // (`RefusedTerminal`), recording nothing; waking the process then would
275        // reschedule a workflow that has already reached its terminal — the
276        // post-terminal wake this gate closes.
277        if self.engine.record_workflow_event(&workflow_id, event)? == RecordOutcome::RefusedTerminal
278        {
279            return Ok(());
280        }
281
282        if let WorkflowResidency::Resident(process) = self.engine.resolve_workflow(&workflow_id)? {
283            self.engine.deliver_workflow_message(
284                process,
285                WorkflowMailboxMessage::TimerFired { timer_id, fire_at },
286            )?;
287        }
288
289        Ok(())
290    }
291
292    /// Route a live reserved-deadline fire to the registered handler.
293    ///
294    /// Called only for a `deadline:{run_id}` timer that passed the liveness
295    /// guard. A missing handler or an unparseable run id is a typed
296    /// [`TimerServiceError::Deadline`] — never a silent generic fire — and the
297    /// handler's own failure is surfaced the same way. The handler re-checks the
298    /// run's terminal under the recorder lock, so it loses cleanly to a
299    /// concurrent completion.
300    async fn fire_deadline(
301        &self,
302        workflow_id: WorkflowId,
303        timer_id: TimerId,
304    ) -> Result<(), TimerServiceError> {
305        let handler = self.deadline_handler.as_ref().ok_or_else(|| {
306            TimerServiceError::Deadline(format!(
307                "no deadline handler registered for {timer_id} on workflow {workflow_id}"
308            ))
309        })?;
310        let run_id = deadline_run_id(&timer_id).ok_or_else(|| {
311            TimerServiceError::Deadline(format!(
312                "malformed deadline timer {timer_id} on workflow {workflow_id}"
313            ))
314        })?;
315        handler
316            .on_deadline_elapsed(workflow_id, run_id)
317            .await
318            .map_err(|error| TimerServiceError::Deadline(error.to_string()))
319    }
320
321    /// Whether the timer is currently live (started and not since retired) in
322    /// the workflow's active run segment, by last-event-wins.
323    ///
324    /// A timer belongs to the run that recorded its `TimerStarted`, and
325    /// anonymous timer identities are run-scoped ordinals that replacement
326    /// runs (continue-as-new) re-allocate from zero. Scoping the check to
327    /// the latest run segment keeps a stale fire from a finished run from
328    /// recording into — or suppressing — the replacement run's identically
329    /// named timer.
330    ///
331    /// Liveness is decided by the *last* timer event for the id (see
332    /// [`live_timers_in_active_segment`]): a re-armed named timer
333    /// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly live
334    /// again rather than judged terminal forever by the earlier `TimerFired`.
335    async fn timer_is_live(
336        &self,
337        workflow_id: &WorkflowId,
338        timer_id: &TimerId,
339    ) -> Result<bool, StoreError> {
340        let history = self.store.read_history(workflow_id).await?;
341        Ok(live_timers_in_active_segment(&history).contains(timer_id))
342    }
343
344    async fn next_envelope(&self, workflow_id: &WorkflowId) -> Result<EventEnvelope, StoreError> {
345        let history = self.store.read_history(workflow_id).await?;
346        let seq = history.iter().map(Event::seq).max().unwrap_or_default() + 1;
347        Ok(EventEnvelope {
348            seq,
349            recorded_at: (self.recorded_at)(),
350            workflow_id: workflow_id.clone(),
351        })
352    }
353}
354
355/// The live timer ids in the workflow's active run segment, by last-event-wins.
356///
357/// Scans forward from the latest `WorkflowStarted` (the active run segment) and
358/// lets the *last* event for each timer id decide its liveness: a `TimerStarted`
359/// (re)arms it, a `TimerFired`/`TimerCancelled` retires it. This means a *named*
360/// timer that fired or was cancelled and then re-armed within the same segment
361/// (`TimerStarted(T), TimerFired(T), TimerStarted(T)`) is correctly reported live
362/// again, rather than judged terminal forever by the earlier terminal event.
363///
364/// Start order is preserved and a timer id started more than once is deduped, so
365/// the result is a stable, history-derived (and therefore replay-deterministic)
366/// view of which timers are outstanding. This is the single liveness model shared
367/// by [`TimerService::timer_is_live`] (firing/cancel guard) and the cancel-path
368/// enumerator in `engine::api`, so the two cannot diverge.
369pub(crate) fn live_timers_in_active_segment(history: &[Event]) -> Vec<TimerId> {
370    let segment_start = history
371        .iter()
372        .rposition(|event| matches!(event, Event::WorkflowStarted { .. }))
373        .unwrap_or(0);
374    let mut live: Vec<TimerId> = Vec::new();
375    for event in &history[segment_start..] {
376        match event {
377            Event::TimerStarted { timer_id, .. } if !live.contains(timer_id) => {
378                live.push(timer_id.clone());
379            }
380            Event::TimerFired { timer_id, .. } | Event::TimerCancelled { timer_id, .. } => {
381                live.retain(|id| id != timer_id);
382            }
383            _ => {}
384        }
385    }
386    live
387}
388
389#[cfg(test)]
390mod tests {
391    use std::sync::Arc;
392
393    use aion_core::{Event, EventEnvelope, RunId, TimerCancelCause, TimerId, WorkflowId};
394    use aion_store::{InMemoryStore, ReadableEventStore, StoreError, WritableEventStore};
395    use chrono::{DateTime, Utc};
396
397    use super::{TimerService, TimerServiceError, live_timers_in_active_segment};
398    use crate::engine_seam::test_support::{
399        DeliveredWorkflowMessage, FakeEngineHandle, FakeEngineOperation,
400    };
401    use crate::engine_seam::{
402        EngineHandle, TimerWheelEntry, WorkflowProcessHandle, WorkflowResidency,
403    };
404    use crate::time::deadline::{DeadlineHandler, DeadlineHandlerError, deadline_timer_id};
405
406    fn instant(offset_seconds: i64) -> DateTime<Utc> {
407        DateTime::from_timestamp(1_700_000_000 + offset_seconds, 0).unwrap_or_default()
408    }
409
410    fn workflow_id() -> WorkflowId {
411        WorkflowId::new_v4()
412    }
413
414    fn timer_id() -> TimerId {
415        TimerId::anonymous(7)
416    }
417
418    fn service() -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
419        let concrete_store = Arc::new(InMemoryStore::default());
420        let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
421        let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
422        let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
423        let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at);
424        (concrete_store, engine, service)
425    }
426
427    fn recorded_at() -> DateTime<Utc> {
428        instant(1)
429    }
430
431    async fn history(
432        store: &InMemoryStore,
433        workflow_id: &WorkflowId,
434    ) -> Result<Vec<Event>, StoreError> {
435        store.read_history(workflow_id).await
436    }
437
438    fn count_timer_fired(events: &[Event], timer_id: &TimerId) -> usize {
439        events
440            .iter()
441            .filter(|event| {
442                matches!(event, Event::TimerFired { timer_id: recorded, .. } if recorded == timer_id)
443            })
444            .count()
445    }
446
447    fn timer_started_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
448        Event::TimerStarted {
449            envelope: EventEnvelope {
450                seq,
451                recorded_at: instant(0),
452                workflow_id: workflow_id.clone(),
453            },
454            timer_id: timer_id.clone(),
455            fire_at: instant(5),
456        }
457    }
458
459    fn workflow_started_event(workflow_id: &WorkflowId, seq: u64) -> Event {
460        Event::WorkflowStarted {
461            envelope: EventEnvelope {
462                seq,
463                recorded_at: instant(0),
464                workflow_id: workflow_id.clone(),
465            },
466            workflow_type: "fixture".to_owned(),
467            input: aion_core::Payload::new(aion_core::ContentType::Json, b"null".to_vec()),
468            run_id: aion_core::RunId::new_v4(),
469            parent_run_id: None,
470            package_version: aion_core::PackageVersion::new("a".repeat(64)),
471        }
472    }
473
474    fn timer_fired_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
475        Event::TimerFired {
476            envelope: EventEnvelope {
477                seq,
478                recorded_at: instant(0),
479                workflow_id: workflow_id.clone(),
480            },
481            timer_id: timer_id.clone(),
482        }
483    }
484
485    fn timer_cancelled_event(workflow_id: &WorkflowId, timer_id: &TimerId, seq: u64) -> Event {
486        Event::TimerCancelled {
487            cause: TimerCancelCause::WorkflowIntent,
488            envelope: EventEnvelope {
489                seq,
490                recorded_at: instant(0),
491                workflow_id: workflow_id.clone(),
492            },
493            timer_id: timer_id.clone(),
494        }
495    }
496
497    fn make_named(name: &str) -> TimerId {
498        // The name is a non-empty literal, so construction never fails; the
499        // anonymous fallback only exists to keep the helper total without an
500        // `unwrap`/`expect` (disallowed by clippy in this crate).
501        TimerId::named(name).unwrap_or_else(|_| TimerId::anonymous(0))
502    }
503
504    fn named_timer_id() -> TimerId {
505        make_named("review-deadline")
506    }
507
508    // --- `live_timers_in_active_segment` / `timer_is_live` semantics ---
509
510    #[test]
511    fn started_timer_is_live() {
512        let workflow_id = workflow_id();
513        let timer_id = named_timer_id();
514        let history = vec![
515            workflow_started_event(&workflow_id, 0),
516            timer_started_event(&workflow_id, &timer_id, 1),
517        ];
518        assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
519    }
520
521    #[test]
522    fn started_then_fired_timer_is_dead() {
523        let workflow_id = workflow_id();
524        let timer_id = named_timer_id();
525        let history = vec![
526            workflow_started_event(&workflow_id, 0),
527            timer_started_event(&workflow_id, &timer_id, 1),
528            timer_fired_event(&workflow_id, &timer_id, 2),
529        ];
530        assert!(live_timers_in_active_segment(&history).is_empty());
531    }
532
533    #[test]
534    fn started_then_cancelled_timer_is_dead() {
535        let workflow_id = workflow_id();
536        let timer_id = named_timer_id();
537        let history = vec![
538            workflow_started_event(&workflow_id, 0),
539            timer_started_event(&workflow_id, &timer_id, 1),
540            timer_cancelled_event(&workflow_id, &timer_id, 2),
541        ];
542        assert!(live_timers_in_active_segment(&history).is_empty());
543    }
544
545    #[test]
546    fn restarted_named_timer_after_fire_is_live() {
547        // The bug fix: a named timer that fired then was re-armed in the same run
548        // segment must be live again (last-event-wins), not judged terminal forever
549        // by the earlier `TimerFired`.
550        let workflow_id = workflow_id();
551        let timer_id = named_timer_id();
552        let history = vec![
553            workflow_started_event(&workflow_id, 0),
554            timer_started_event(&workflow_id, &timer_id, 1),
555            timer_fired_event(&workflow_id, &timer_id, 2),
556            timer_started_event(&workflow_id, &timer_id, 3),
557        ];
558        assert_eq!(
559            live_timers_in_active_segment(&history),
560            vec![timer_id],
561            "a re-armed named timer is live again"
562        );
563    }
564
565    #[test]
566    fn restarted_named_timer_after_cancel_is_live() {
567        let workflow_id = workflow_id();
568        let timer_id = named_timer_id();
569        let history = vec![
570            workflow_started_event(&workflow_id, 0),
571            timer_started_event(&workflow_id, &timer_id, 1),
572            timer_cancelled_event(&workflow_id, &timer_id, 2),
573            timer_started_event(&workflow_id, &timer_id, 3),
574        ];
575        assert_eq!(live_timers_in_active_segment(&history), vec![timer_id]);
576    }
577
578    #[test]
579    fn prior_run_segment_timer_is_not_live() {
580        // A timer started in a run segment that a later `WorkflowStarted` closed
581        // (continue-as-new) is out of scope for the active segment.
582        let workflow_id = workflow_id();
583        let prior = named_timer_id();
584        let current = make_named("current-deadline");
585        let history = vec![
586            workflow_started_event(&workflow_id, 0),
587            timer_started_event(&workflow_id, &prior, 1),
588            // New run segment begins; the prior timer must not be surfaced.
589            workflow_started_event(&workflow_id, 2),
590            timer_started_event(&workflow_id, &current, 3),
591        ];
592        assert_eq!(live_timers_in_active_segment(&history), vec![current]);
593    }
594
595    #[tokio::test]
596    async fn re_armed_named_timer_fires_again() -> Result<(), TimerServiceError> {
597        // End-to-end firing-path guard: with last-event-wins, a re-armed named
598        // timer is live, so `fire_timer` records a second `TimerFired` and
599        // delivers it — rather than silently no-opping under the old
600        // `any`-semantics.
601        let process = WorkflowProcessHandle::new(42);
602        let (store, engine, service) = service();
603        let workflow_id = workflow_id();
604        let timer_id = named_timer_id();
605        let fire_at = instant(110);
606        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
607        engine.record_workflow_event(
608            &workflow_id,
609            timer_started_event(&workflow_id, &timer_id, 1),
610        )?;
611        engine
612            .record_workflow_event(&workflow_id, timer_fired_event(&workflow_id, &timer_id, 2))?;
613        engine.record_workflow_event(
614            &workflow_id,
615            timer_started_event(&workflow_id, &timer_id, 3),
616        )?;
617
618        service
619            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
620            .await?;
621
622        assert_eq!(
623            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
624            2,
625            "the re-armed timer fires again, recording a second TimerFired"
626        );
627        assert_eq!(engine.delivered_messages()?.len(), 1);
628        Ok(())
629    }
630
631    #[tokio::test]
632    async fn schedule_records_timer_row_without_timer_started_event()
633    -> Result<(), TimerServiceError> {
634        let (store, _engine, service) = service();
635        let workflow_id = workflow_id();
636        let timer_id = timer_id();
637        let fire_at = instant(10);
638
639        service
640            .schedule(workflow_id.clone(), timer_id.clone(), fire_at)
641            .await?;
642
643        let expired = store.expired_timers(fire_at).await?;
644        assert_eq!(expired.len(), 1);
645        assert_eq!(expired[0].workflow_id, workflow_id);
646        assert_eq!(expired[0].timer_id, timer_id);
647        assert_eq!(expired[0].fire_at, fire_at);
648
649        assert!(history(&store, &workflow_id).await?.is_empty());
650        Ok(())
651    }
652
653    #[tokio::test]
654    async fn schedule_arms_wheel_for_resident_workflow() -> Result<(), TimerServiceError> {
655        let process = WorkflowProcessHandle::new(42);
656        let (_store, engine, service) = service();
657        let workflow_id = workflow_id();
658        let timer_id = timer_id();
659        let fire_at = instant(20);
660        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
661
662        service
663            .schedule(workflow_id, timer_id.clone(), fire_at)
664            .await?;
665
666        assert_eq!(
667            engine.armed_timers()?,
668            vec![TimerWheelEntry {
669                process,
670                timer_id,
671                fire_at
672            }]
673        );
674        Ok(())
675    }
676
677    #[tokio::test]
678    async fn schedule_for_nonresident_records_without_arming() -> Result<(), TimerServiceError> {
679        let (store, engine, service) = service();
680        let workflow_id = workflow_id();
681        let timer_id = timer_id();
682        let fire_at = instant(30);
683        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
684
685        service
686            .schedule(workflow_id.clone(), timer_id, fire_at)
687            .await?;
688
689        assert!(engine.armed_timers()?.is_empty());
690        assert!(history(&store, &workflow_id).await?.is_empty());
691        Ok(())
692    }
693
694    #[tokio::test]
695    async fn fire_records_timer_fired_then_delivers_mailbox_message()
696    -> Result<(), TimerServiceError> {
697        let process = WorkflowProcessHandle::new(42);
698        let (store, engine, service) = service();
699        let workflow_id = workflow_id();
700        let timer_id = timer_id();
701        let fire_at = instant(40);
702        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
703        engine.record_workflow_event(
704            &workflow_id,
705            timer_started_event(&workflow_id, &timer_id, 1),
706        )?;
707
708        service
709            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
710            .await?;
711
712        assert_eq!(
713            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
714            1
715        );
716        assert_eq!(
717            engine.delivered_messages()?,
718            vec![(
719                process,
720                DeliveredWorkflowMessage::TimerFired {
721                    timer_id: timer_id.clone(),
722                    fire_at
723                }
724            )]
725        );
726        assert!(matches!(
727            engine.operations()?.as_slice(),
728            [
729                FakeEngineOperation::EventRecorded {
730                    event: Event::TimerStarted { .. },
731                    ..
732                },
733                FakeEngineOperation::EventRecorded {
734                    workflow_id: recorded_workflow_id,
735                    event: Event::TimerFired { timer_id: recorded_timer_id, .. },
736                },
737                FakeEngineOperation::Delivered {
738                    process: delivered_process,
739                    message: DeliveredWorkflowMessage::TimerFired { timer_id: delivered_timer_id, .. },
740                }
741            ] if recorded_workflow_id == &workflow_id
742                && recorded_timer_id == &timer_id
743                && delivered_process == &process
744                && delivered_timer_id == &timer_id
745        ));
746        Ok(())
747    }
748
749    #[tokio::test]
750    async fn fire_records_without_delivery_when_workflow_becomes_nonresident()
751    -> Result<(), TimerServiceError> {
752        let (store, engine, service) = service();
753        let workflow_id = workflow_id();
754        let timer_id = timer_id();
755        let fire_at = instant(50);
756        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
757        engine.record_workflow_event(
758            &workflow_id,
759            timer_started_event(&workflow_id, &timer_id, 1),
760        )?;
761
762        service
763            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
764            .await?;
765
766        assert_eq!(
767            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
768            1
769        );
770        assert!(engine.delivered_messages()?.is_empty());
771        Ok(())
772    }
773
774    #[tokio::test]
775    async fn firing_same_timer_twice_records_and_delivers_once() -> Result<(), TimerServiceError> {
776        let process = WorkflowProcessHandle::new(42);
777        let (store, engine, service) = service();
778        let workflow_id = workflow_id();
779        let timer_id = timer_id();
780        let fire_at = instant(60);
781        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
782        engine.record_workflow_event(
783            &workflow_id,
784            timer_started_event(&workflow_id, &timer_id, 1),
785        )?;
786
787        service
788            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
789            .await?;
790        service
791            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
792            .await?;
793
794        assert_eq!(
795            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
796            1
797        );
798        assert_eq!(engine.delivered_messages()?.len(), 1);
799        Ok(())
800    }
801
802    #[tokio::test]
803    async fn firing_cancelled_timer_is_noop() -> Result<(), TimerServiceError> {
804        let process = WorkflowProcessHandle::new(42);
805        let (store, engine, service) = service();
806        let workflow_id = workflow_id();
807        let timer_id = timer_id();
808        let fire_at = instant(70);
809        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
810        engine.record_workflow_event(
811            &workflow_id,
812            timer_started_event(&workflow_id, &timer_id, 1),
813        )?;
814        let cancelled = Event::TimerCancelled {
815            cause: TimerCancelCause::WorkflowIntent,
816            envelope: EventEnvelope {
817                seq: 2,
818                recorded_at: instant(69),
819                workflow_id: workflow_id.clone(),
820            },
821            timer_id: timer_id.clone(),
822        };
823        engine.record_workflow_event(&workflow_id, cancelled)?;
824
825        service
826            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
827            .await?;
828
829        let history = history(&store, &workflow_id).await?;
830        assert_eq!(count_timer_fired(&history, &timer_id), 0);
831        assert!(engine.delivered_messages()?.is_empty());
832        Ok(())
833    }
834
835    #[tokio::test]
836    async fn fire_resolves_residency_at_fire_time() -> Result<(), TimerServiceError> {
837        let process = WorkflowProcessHandle::new(42);
838        let (store, engine, service) = service();
839        let workflow_id = workflow_id();
840        let timer_id = timer_id();
841        let fire_at = instant(80);
842
843        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
844        engine.set_residency(workflow_id.clone(), WorkflowResidency::NonResident)?;
845        engine.record_workflow_event(
846            &workflow_id,
847            timer_started_event(&workflow_id, &timer_id, 1),
848        )?;
849        service
850            .fire_timer(workflow_id.clone(), timer_id.clone(), fire_at)
851            .await?;
852
853        assert_eq!(
854            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
855            1
856        );
857        assert!(engine.delivered_messages()?.is_empty());
858        Ok(())
859    }
860
861    #[tokio::test]
862    async fn firing_unstarted_timer_records_nothing() -> Result<(), TimerServiceError> {
863        let process = WorkflowProcessHandle::new(42);
864        let (store, engine, service) = service();
865        let workflow_id = workflow_id();
866        let timer_id = timer_id();
867        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
868
869        service
870            .fire_timer(workflow_id.clone(), timer_id.clone(), instant(90))
871            .await?;
872
873        assert!(history(&store, &workflow_id).await?.is_empty());
874        assert!(engine.delivered_messages()?.is_empty());
875        Ok(())
876    }
877
878    /// A deadline handler that records each fire and can be told to fail.
879    struct RecordingDeadlineHandler {
880        calls: std::sync::Mutex<Vec<(WorkflowId, RunId)>>,
881        fail: bool,
882    }
883
884    impl RecordingDeadlineHandler {
885        fn new(fail: bool) -> Self {
886            Self {
887                calls: std::sync::Mutex::new(Vec::new()),
888                fail,
889            }
890        }
891
892        fn calls(&self) -> Result<Vec<(WorkflowId, RunId)>, TimerServiceError> {
893            self.calls
894                .lock()
895                .map(|calls| calls.clone())
896                .map_err(|error| TimerServiceError::Deadline(error.to_string()))
897        }
898    }
899
900    #[async_trait::async_trait]
901    impl DeadlineHandler for RecordingDeadlineHandler {
902        async fn on_deadline_elapsed(
903            &self,
904            workflow_id: WorkflowId,
905            run_id: RunId,
906        ) -> Result<(), DeadlineHandlerError> {
907            self.calls
908                .lock()
909                .map_err(|error| DeadlineHandlerError(error.to_string()))?
910                .push((workflow_id, run_id));
911            if self.fail {
912                Err(DeadlineHandlerError(
913                    "deliberate handler failure".to_owned(),
914                ))
915            } else {
916                Ok(())
917            }
918        }
919    }
920
921    fn service_with_handler(
922        handler: Arc<dyn DeadlineHandler>,
923    ) -> (Arc<InMemoryStore>, Arc<FakeEngineHandle>, TimerService) {
924        let concrete_store = Arc::new(InMemoryStore::default());
925        let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
926        let readable_store: Arc<dyn ReadableEventStore> = concrete_store.clone();
927        let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
928        let service = TimerService::with_recorded_at(engine.clone(), readable_store, recorded_at)
929            .with_deadline_handler(handler);
930        (concrete_store, engine, service)
931    }
932
933    /// A live reserved deadline fire is demuxed to the registered handler with
934    /// the id-encoded run, and records NO `TimerFired` and delivers nothing.
935    #[tokio::test]
936    async fn deadline_fire_routes_to_handler_and_records_no_timer_fired()
937    -> Result<(), TimerServiceError> {
938        let run_id = RunId::new_v4();
939        let deadline_id = deadline_timer_id(&run_id)
940            .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
941        let handler = Arc::new(RecordingDeadlineHandler::new(false));
942        let (store, engine, service) = service_with_handler(handler.clone());
943        let workflow_id = workflow_id();
944        let fire_at = instant(120);
945        engine.set_residency(
946            workflow_id.clone(),
947            WorkflowResidency::Resident(WorkflowProcessHandle::new(9)),
948        )?;
949        engine.record_workflow_event(
950            &workflow_id,
951            timer_started_event(&workflow_id, &deadline_id, 1),
952        )?;
953
954        service
955            .fire_timer(workflow_id.clone(), deadline_id.clone(), fire_at)
956            .await?;
957
958        assert_eq!(handler.calls()?, vec![(workflow_id.clone(), run_id)]);
959        assert_eq!(
960            count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
961            0,
962            "a deadline fire never records TimerFired"
963        );
964        assert!(engine.delivered_messages()?.is_empty());
965        Ok(())
966    }
967
968    /// A deadline fire with no handler registered is a typed error — never a
969    /// silent generic fire.
970    #[tokio::test]
971    async fn deadline_fire_without_handler_is_typed_error() -> Result<(), TimerServiceError> {
972        let run_id = RunId::new_v4();
973        let deadline_id = deadline_timer_id(&run_id)
974            .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
975        let (store, engine, service) = service();
976        let workflow_id = workflow_id();
977        engine.record_workflow_event(
978            &workflow_id,
979            timer_started_event(&workflow_id, &deadline_id, 1),
980        )?;
981
982        let result = service
983            .fire_timer(workflow_id.clone(), deadline_id.clone(), instant(120))
984            .await;
985
986        assert!(
987            matches!(result, Err(TimerServiceError::Deadline(_))),
988            "unhandled deadline fire must be a typed error, got {result:?}"
989        );
990        assert_eq!(
991            count_timer_fired(&history(&store, &workflow_id).await?, &deadline_id),
992            0
993        );
994        Ok(())
995    }
996
997    /// A handler failure surfaces as a typed deadline error to the caller.
998    #[tokio::test]
999    async fn deadline_handler_failure_surfaces_as_typed_error() -> Result<(), TimerServiceError> {
1000        let run_id = RunId::new_v4();
1001        let deadline_id = deadline_timer_id(&run_id)
1002            .map_err(|error| TimerServiceError::Deadline(error.to_string()))?;
1003        let handler = Arc::new(RecordingDeadlineHandler::new(true));
1004        let (_store, engine, service) = service_with_handler(handler);
1005        let workflow_id = workflow_id();
1006        engine.record_workflow_event(
1007            &workflow_id,
1008            timer_started_event(&workflow_id, &deadline_id, 1),
1009        )?;
1010
1011        let result = service
1012            .fire_timer(workflow_id, deadline_id, instant(120))
1013            .await;
1014
1015        assert!(matches!(result, Err(TimerServiceError::Deadline(_))));
1016        Ok(())
1017    }
1018
1019    /// A fire the recorder refuses as a post-terminal late arrival records
1020    /// nothing AND delivers no wake. Mutation-sensitive: the timer is live so the
1021    /// pre-check passes and the fire reaches the recorder seam, which returns
1022    /// `RefusedTerminal`; delivering the mailbox wake regardless of that outcome
1023    /// would reschedule a terminated workflow and fail this test.
1024    #[tokio::test]
1025    async fn refused_terminal_fire_records_nothing_and_delivers_no_wake()
1026    -> Result<(), TimerServiceError> {
1027        let process = WorkflowProcessHandle::new(42);
1028        let (store, engine, service) = service();
1029        let workflow_id = workflow_id();
1030        let timer_id = timer_id();
1031        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1032        engine.record_workflow_event(
1033            &workflow_id,
1034            timer_started_event(&workflow_id, &timer_id, 1),
1035        )?;
1036        engine.refuse_next_record_as_terminal()?;
1037
1038        service
1039            .fire_timer(workflow_id.clone(), timer_id.clone(), instant(130))
1040            .await?;
1041
1042        assert_eq!(
1043            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1044            0,
1045            "a refused fire records no TimerFired"
1046        );
1047        assert!(
1048            engine.delivered_messages()?.is_empty(),
1049            "a refused fire delivers no wake"
1050        );
1051        Ok(())
1052    }
1053
1054    /// Two services obtained separately but sharing ONE terminal-update
1055    /// coordinator (as the production bridge hands out) serialize a cancel and a
1056    /// fire of the same timer: exactly one terminal timer event is recorded, never
1057    /// both. A `Barrier` forces genuine overlap — both actors are released
1058    /// together after setup — and the loop runs each direction. Mutation-sensitive:
1059    /// a per-service coordinator would let both read the timer live and record a
1060    /// `TimerFired` AND a `TimerCancelled`.
1061    #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
1062    async fn shared_coordinator_serializes_cancel_and_fire_across_services()
1063    -> Result<(), TimerServiceError> {
1064        use dashmap::DashSet;
1065        use tokio::sync::Barrier;
1066
1067        for _ in 0..20 {
1068            let process = WorkflowProcessHandle::new(42);
1069            let concrete_store = Arc::new(InMemoryStore::default());
1070            let recorder_store: Arc<dyn WritableEventStore> = concrete_store.clone();
1071            let readable: Arc<dyn ReadableEventStore> = concrete_store.clone();
1072            let engine = Arc::new(FakeEngineHandle::recording_to(recorder_store));
1073            let coordinator = Arc::new(DashSet::new());
1074            let service_a =
1075                TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1076                    .with_terminal_updates(Arc::clone(&coordinator));
1077            let service_b =
1078                TimerService::with_recorded_at(engine.clone(), readable.clone(), recorded_at)
1079                    .with_terminal_updates(Arc::clone(&coordinator));
1080
1081            let workflow_id = workflow_id();
1082            let timer_id = timer_id();
1083            let fire_at = instant(200);
1084            engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1085            engine.record_workflow_event(
1086                &workflow_id,
1087                timer_started_event(&workflow_id, &timer_id, 1),
1088            )?;
1089
1090            let gate = Arc::new(Barrier::new(2));
1091            let (cancel_gate, fire_gate) = (Arc::clone(&gate), gate);
1092            let (cancel_wf, cancel_timer) = (workflow_id.clone(), timer_id.clone());
1093            let cancel = async move {
1094                cancel_gate.wait().await;
1095                service_a
1096                    .cancel(cancel_wf, cancel_timer, TimerCancelCause::WorkflowIntent)
1097                    .await
1098            };
1099            let (fire_wf, fire_timer) = (workflow_id.clone(), timer_id.clone());
1100            let fire = async move {
1101                fire_gate.wait().await;
1102                service_b.fire_timer(fire_wf, fire_timer, fire_at).await
1103            };
1104            let (cancel_result, fire_result) = tokio::join!(cancel, fire);
1105            cancel_result?;
1106            fire_result?;
1107
1108            let history = history(&concrete_store, &workflow_id).await?;
1109            let terminal_timer_events = history
1110                .iter()
1111                .filter(|event| {
1112                    matches!(
1113                        event,
1114                        Event::TimerFired { timer_id: recorded, .. }
1115                        | Event::TimerCancelled { timer_id: recorded, .. }
1116                            if recorded == &timer_id
1117                    )
1118                })
1119                .count();
1120            assert_eq!(
1121                terminal_timer_events, 1,
1122                "first-recorded wins across shared services: {history:#?}"
1123            );
1124        }
1125        Ok(())
1126    }
1127
1128    #[tokio::test]
1129    async fn firing_prior_run_timer_after_continue_as_new_is_noop() -> Result<(), TimerServiceError>
1130    {
1131        let process = WorkflowProcessHandle::new(42);
1132        let (store, engine, service) = service();
1133        let workflow_id = workflow_id();
1134        let timer_id = timer_id();
1135        engine.set_residency(workflow_id.clone(), WorkflowResidency::Resident(process))?;
1136        // Run 1 started the timer; run 2's WorkflowStarted closes that segment.
1137        engine.record_workflow_event(
1138            &workflow_id,
1139            timer_started_event(&workflow_id, &timer_id, 1),
1140        )?;
1141        engine.record_workflow_event(&workflow_id, workflow_started_event(&workflow_id, 2))?;
1142
1143        service
1144            .fire_timer(workflow_id.clone(), timer_id.clone(), instant(100))
1145            .await?;
1146
1147        assert_eq!(
1148            count_timer_fired(&history(&store, &workflow_id).await?, &timer_id),
1149            0
1150        );
1151        assert!(engine.delivered_messages()?.is_empty());
1152        Ok(())
1153    }
1154}