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