Skip to main content

cratestack_axum/idempotency/
record.rs

1//! Persisted record + reservation-outcome state machine.
2
3use std::time::SystemTime;
4
5/// Persisted idempotency record returned on a replay. Banks need an
6/// invariant view of the captured response — the store rebuilds this from
7/// its persisted columns when the second caller asks to replay.
8///
9/// `response_headers` is an opaque blob produced by [`super::encode_headers`]
10/// at capture time and consumed by [`super::decode_headers`] on replay. The
11/// blob carries every end-to-end header the handler returned, including
12/// `Location`, `ETag`, cache directives, and `Content-Type` — replaying
13/// only the status + body would silently drop these and give a retry
14/// different observable behaviour from the original execution.
15#[derive(Debug, Clone)]
16pub struct IdempotencyRecord {
17    pub key: String,
18    pub principal_fingerprint: String,
19    pub request_hash: [u8; 32],
20    pub response_status: u16,
21    pub response_headers: Vec<u8>,
22    pub response_body: Vec<u8>,
23    pub created_at: SystemTime,
24    pub expires_at: SystemTime,
25}
26
27/// Outcome of an atomic `reserve_or_fetch` call.
28///
29/// The middleware uses this state machine to decide whether to run the
30/// handler, replay a cached response, or reject. Exactly one caller per
31/// `(principal, key)` ever gets `Reserved` — that's the contract banking
32/// flows like transfers rely on.
33#[derive(Debug, Clone)]
34pub enum ReservationOutcome {
35    /// This caller claimed the key. It MUST run the handler and then
36    /// invoke `complete` (success) or `release` (give up the
37    /// reservation so a retry can re-acquire). The `token` uniquely
38    /// identifies THIS reservation — `complete` and `release` only
39    /// write when the row still carries the same token, so a handler
40    /// that overran the TTL and had its row reclaimed by a retry
41    /// can't poison the newer reservation.
42    Reserved { token: uuid::Uuid },
43    /// Another caller already completed an execution with the same
44    /// request hash. The middleware returns the cached response.
45    Replay(IdempotencyRecord),
46    /// Another caller is currently executing under the same key + hash.
47    /// The middleware returns `409 Conflict` with `Retry-After: 1` so
48    /// the client retries shortly.
49    InFlight,
50    /// Same key was claimed by a different request body — the IETF
51    /// draft's `idempotency_key_conflict` (422).
52    Conflict,
53}