RequestSeal.Replay.ETS (RequestSeal v0.5.2)

Copy Markdown View Source

Local atomic replay store owned by a caller-supervised GenServer.

The protected ETS table contains a reserved counter and namespace/key entries. Only this adapter may mutate it. Claims and explicit sweeps are serialized by its owner, preventing delete/insert races and enforcing an exact capacity. Duplicate claims never refresh retention. Owner termination loses all claims; this is not a durable or distributed store. Importing the module starts nothing. 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

Functions

Returns a specification to start this module under a supervisor.

Atomically store claim unless the store already holds its namespace and key.

Start a local store owned by the calling process, normally under a supervisor.

Reference an existing caller-owned store for RequestSeal.Replay.claim/3 or a replay policy.

Explicitly evict claims whose exclusive retention end is at or before now, with a 5,000 ms deadline.

Evict claims whose exclusive retention end is at or before now.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

claim(pid, claim, context)

Atomically store claim unless the store already holds its namespace and key.

Implements RequestSeal.Replay.claim/3; most callers use RequestSeal.Replay.claim/3, which supplies the context and maps results to RequestSeal.Replay.Error. A duplicate never refreshes retention, and a stored entry stays until a sweep removes it, even after its retention end.

Parameters

Returns

  • :claimed - the entry was stored.
  • :already_claimed - the namespace and key are already stored (also when the store is full).
  • {:error, :full} - the store holds max_entries claims.
  • {:error, :timeout} - the deadline expired before or during the call.
  • {:error, :unavailable} - the owner process is not running.
  • {:error, :failure} - the claim fails RequestSeal.Replay.Claim.valid?/1, or an argument is not a claim or context.

Examples

iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 1)
iex> context = %RequestSeal.Custody.Context{
...>   owner: self(),
...>   deadline: System.monotonic_time(:millisecond) + 1_000
...> }
iex> claim = %RequestSeal.Replay.Claim{namespace: "builds", key: "job-41", retain_until: 100}
iex> RequestSeal.Replay.ETS.claim(pid, claim, context)
:claimed
iex> RequestSeal.Replay.ETS.claim(pid, claim, context)
:already_claimed
iex> RequestSeal.Replay.ETS.claim(pid, %{claim | key: "job-42"}, context)
{:error, :full}
iex> GenServer.stop(pid)
:ok

start_link(arg1)

Start a local store owned by the calling process, normally under a supervisor.

The process is linked to the caller. Use {RequestSeal.Replay.ETS, max_entries: n} as a child specification.

Parameters

  • opts - keyword list; see Options.

Options

  • :max_entries - required, and the only accepted option: the exact number of claims the store holds, 1 through 10,000,000. A claim beyond it returns {:error, :full} until a sweep removes entries.

Returns

Examples

iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 10_000)
iex> is_pid(pid)
true
iex> GenServer.stop(pid)
:ok
iex> RequestSeal.Replay.ETS.start_link(max_entries: 0)
{:error, :invalid_options}

store(pid)

@spec store(pid()) :: RequestSeal.Replay.Store.t()

Reference an existing caller-owned store for RequestSeal.Replay.claim/3 or a replay policy.

Building the reference does not contact the store.

Parameters

Returns

Examples

iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 100)
iex> store = RequestSeal.Replay.ETS.store(pid)
iex> store.adapter
RequestSeal.Replay.ETS
iex> GenServer.stop(pid)
:ok

sweep(pid, now)

Explicitly evict claims whose exclusive retention end is at or before now, with a 5,000 ms deadline.

Same as sweep/3 with a context whose deadline is 5,000 milliseconds from now.

Parameters

  • pid - the store owner from start_link/1.
  • now - Unix seconds from 0 through 253,402,300,799.

Returns

Same as sweep/3.

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: "messages", key: "m-1", retain_until: 1_000}
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.ETS.sweep(pid, 999)
{:ok, 0}
iex> RequestSeal.Replay.ETS.sweep(pid, 1_000)
{:ok, 1}
iex> RequestSeal.Replay.claim(store, claim, timeout: 1_000)
:claimed
iex> RequestSeal.Replay.ETS.sweep(pid, -1)
{:error, :failure}
iex> GenServer.stop(pid)
:ok

sweep(pid, now, context)

Evict claims whose exclusive retention end is at or before now.

Implements RequestSeal.Replay.sweep/3. Sweeps are serialized with claims by the store owner.

Parameters

Returns

  • {:ok, removed} - the number of claims deleted.
  • {:error, :timeout} - the deadline expired before or during the call.
  • {:error, :unavailable} - the owner process is not running.
  • {:error, :failure} - now is outside the range, or context is not a RequestSeal.Custody.Context.

Examples

iex> {:ok, pid} = RequestSeal.Replay.ETS.start_link(max_entries: 100)
iex> context = %RequestSeal.Custody.Context{
...>   owner: self(),
...>   deadline: System.monotonic_time(:millisecond) + 1_000
...> }
iex> claim = %RequestSeal.Replay.Claim{namespace: "crawl", key: "page-9", retain_until: 50}
iex> RequestSeal.Replay.ETS.claim(pid, claim, context)
:claimed
iex> RequestSeal.Replay.ETS.sweep(pid, 50, context)
{:ok, 1}
iex> RequestSeal.Replay.ETS.sweep(pid, 253_402_300_800, context)
{:error, :failure}
iex> GenServer.stop(pid)
:ok