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}