Skip to main content

aion_store/
timer.rs

1//! `TimerEntry` and timer-facing types.
2
3use aion_core::{TimerId, WorkflowId};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7/// Durable timer record returned by [`crate::ReadableEventStore::expired_timers`].
8#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
9pub struct TimerEntry {
10    /// Workflow that owns the timer.
11    pub workflow_id: WorkflowId,
12    /// Timer identifier within the owning workflow.
13    pub timer_id: TimerId,
14    /// Instant at which the timer is due to fire.
15    pub fire_at: DateTime<Utc>,
16    /// The workflow-history sequence of the `TimerStarted` event that armed
17    /// this row, or `0` for an arming that records no `TimerStarted` (the
18    /// schedule coordinator's trigger timers — sequence numbers start at 1,
19    /// so `0` can never collide with a recorded arming).
20    ///
21    /// This is the row-identity component that makes value-conditional
22    /// retirement discriminate a RE-ARM TO THE IDENTICAL INSTANT: without it,
23    /// a named timer re-armed to the same `fire_at` encodes byte-identically
24    /// to the consumed arming, so a stale retirement deletes the live
25    /// replacement's row — and a past-due replacement is never re-armed by
26    /// the boot sweep (which only restores future armings), a permanently
27    /// lost wake. Every re-arm records a new `TimerStarted` with a strictly
28    /// higher sequence, so `(fire_at, armed_seq)` is unique per arming. The
29    /// store treats the value as an opaque identity component: it never
30    /// orders or interprets it.
31    ///
32    /// `serde(default)`: a pre-0.25 store's rows carry no `armed_seq`, and a
33    /// decoder that refuses them bricks the boot of every upgraded server —
34    /// the 2026-08-26 v0.25.0 upgrade failure, hit on the first real store
35    /// this release met. `0` is the documented no-recorded-arming value, so a
36    /// legacy row decodes to a valid identity. A legacy row's BYTES still
37    /// predate this field, so value-conditional retirement must also accept
38    /// the legacy encoding; the store backends own that half.
39    #[serde(default)]
40    pub armed_seq: u64,
41}
42
43/// The outcome of a value-conditional
44/// [`retire_timer`](crate::ReadableEventStore::retire_timer) call.
45///
46/// Both variants are success — the distinction exists so callers (the boot
47/// sweep's counters in particular) can report what actually happened to the
48/// row instead of counting attempts as retirements.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum TimerRetirement {
51    /// The key no longer holds the retired arming's row: it was deleted by
52    /// this call, or was already gone (never scheduled, or retired earlier —
53    /// the idempotent shape). A distributed backend reports an absent key as
54    /// `Retired` too: its tombstone lands unconditionally on an empty key.
55    Retired,
56    /// A DIFFERENT arming now owns the key (same timer name, new
57    /// `(fire_at, armed_seq)` identity — possibly the SAME instant re-armed
58    /// under a newer `TimerStarted`). The replacement's row was left
59    /// untouched — it is the re-armed timer's only durable claim to a
60    /// recovery fire.
61    Superseded,
62}