aion-store 0.27.1

Persistence contracts and in-memory event stores for Aion durable workflows.
Documentation
//! `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,
}