RequestSeal.Replay behaviour (RequestSeal v0.5.2)

Copy Markdown View Source

Atomic whole-envelope replay port with caller-owned stores and commitments.

Adapters implement claim/3 and sweep/3. Claiming never evicts entries. Generic and Web Bot Auth verification reject a derived retention end above 253,402,300,799 Unix seconds with RequestSeal.Error reason :retention_exceeded at :replay before commitment or storage. Such claims cannot be swept. Direct store callers still receive the existing invalid-claim failure; RequestSeal.Replay.Claim.valid?/1 rejects them. Negative retention ends are already expired. A single monotonic deadline covers the commitment and store operation; caller death or deadline expiry kills the operation worker. Cancellation cannot undo an already committed claim. No error permits automatic retry. Multi-node operators sweep at now - max_internode_skew to preserve claims while any verifier still accepts the authenticated input. One namespace should use one freshness bound because duplicates never extend retention.

Summary

Callbacks

Atomically record claim in the store ref, unless its namespace and key are already stored.

Delete stored claims whose exclusive retention end is at or before now.

Functions

Submit one atomic claim to a store under a bounded operation deadline.

Callbacks

claim(term, t, t)

@callback claim(term(), RequestSeal.Replay.Claim.t(), RequestSeal.Custody.Context.t()) ::
  :claimed
  | :already_claimed
  | {:error, :unavailable | :timeout | :full | :failure}

Atomically record claim in the store ref, unless its namespace and key are already stored.

RequestSeal.Replay.claim/3 validates the claim and builds context before calling the adapter. An adapter honors context as the operation deadline and never evicts or refreshes an existing entry while claiming.

Parameters

Returns

  • :claimed - the entry was stored by this call.
  • :already_claimed - an entry with the same namespace and key is stored.
  • {:error, :unavailable} - the store could not be reached.
  • {:error, :timeout} - the deadline expired; the claim may have been committed.
  • {:error, :full} - the store is at capacity.
  • {:error, :failure} - any other failure, including an invalid claim or reference.

RequestSeal.Replay.claim/3 maps these to RequestSeal.Replay.Error reasons :store_unavailable, :store_timeout, :store_full and :store_failed; any other adapter result also becomes :store_failed.

sweep(term, .., t)

@callback sweep(term(), 0..253_402_300_799, RequestSeal.Custody.Context.t()) ::
  {:ok, non_neg_integer()} | {:error, atom()}

Delete stored claims whose exclusive retention end is at or before now.

Parameters

  • ref - the adapter's reference, taken from RequestSeal.Replay.Store's ref.
  • now - Unix seconds from 0 through 253,402,300,799, from a clock the caller trusts and reconciles with verification clocks.
  • context - a RequestSeal.Custody.Context carrying the absolute deadline.

Returns

  • {:ok, removed} - the number of entries deleted.
  • {:error, reason} - an adapter-specific atom; the bundled adapters return :failure, :timeout or :unavailable, and RequestSeal.Replay.AshOnetime always returns :externally_managed.

Functions

claim(store, claim, opts)

@spec claim(RequestSeal.Replay.Store.t(), RequestSeal.Replay.Claim.t(), keyword()) ::
  :claimed | :already_claimed | {:error, RequestSeal.Replay.Error.t()}

Submit one atomic claim to a store under a bounded operation deadline.

The claim runs in a monitored worker. Caller death or deadline expiry kills the worker; a store that already committed the claim keeps it.

Parameters

Options

  • :timeout - required, and the only accepted option: operation deadline in milliseconds, 1 through 300,000.

Returns

  • :claimed - this call stored the claim.
  • :already_claimed - the store already holds the namespace and key.
  • {:error, %RequestSeal.Replay.Error{}} - reasons this function can return: :invalid_options, :invalid_store, :invalid_claim, :store_unavailable, :store_timeout, :store_full, :store_failed. retryable is always false. Meanings: RequestSeal.Replay.Error.

Examples

iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 100)
iex> store = RequestSeal.Replay.ETS.store(pid)
iex> claim = %RequestSeal.Replay.Claim{
...>   namespace: "orders",
...>   key: "nonce-7f3a",
...>   retain_until: 1_900_000_000
...> }
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:already_claimed
iex> RequestSeal.Replay.claim(store, %{claim | namespace: "telemetry"}, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.claim(store, claim, timeout: 0)
{:error, %RequestSeal.Replay.Error{reason: :invalid_options, retryable: false}}
iex> RequestSeal.Replay.claim(store, %{claim | key: ""}, timeout: 1_000)
{:error, %RequestSeal.Replay.Error{reason: :invalid_claim, retryable: false}}
iex> GenServer.stop(pid)
:ok