AttestoPhoenix.Store.Sweeper (AttestoPhoenix v0.6.1)

Copy Markdown View Source

Optional periodic housekeeping GenServer that deletes expired rows from the Ecto-backed authorization-code, refresh-token, DPoP-nonce, and DPoP-replay tables.

Each of these tables carries an expires_at column whose semantics are fixed by the relevant RFC:

  • authorization codes - RFC 6749 §4.1.2 ("The authorization code MUST expire shortly after it is issued") and §10.5 (codes are short-lived, single-use).
  • refresh tokens - RFC 6749 §1.5 / §6 (refresh tokens MAY expire); the stored expiry bounds the credential's lifetime.
  • server-issued DPoP nonces - RFC 9449 §8 / §9 (the nonce the resource or authorization server requires the client to echo is time-bounded).
  • DPoP proof jti replay records - RFC 9449 §11.1 (a jti need only be remembered for the proof iat acceptance window; past that window the record is dead weight).

Correctness vs. housekeeping

Sweeping is not required for correctness. Every store re-validates expires_at against the current time on read, so an expired row that has not yet been swept is never honored: an expired authorization code is rejected, an expired nonce is rejected, and an expired replay record no longer blocks a fresh jti. The sweeper exists only to bound table growth by reclaiming rows that can no longer affect any decision. It is therefore safe to run on any interval, or not at all.

This is generic TTL housekeeping: it issues a single DELETE ... WHERE expires_at < $now per swept table and makes no assumption about how, where, or by which process the host deploys it.

Comparison boundary (fail-closed)

Deletion uses a strict < comparison against a single DateTime captured once per sweep (DateTime.utc_now/0) and reused across every table, so a sweep applies one consistent boundary. A row whose expires_at equals "now" is retained, never deleted, so the sweeper can only ever remove rows that the stores themselves already treat as expired. The sweeper widens no acceptance window.

Configuration

All policy is read from AttestoPhoenix.Config; nothing is hardcoded here.

  • :repo - the Ecto.Repo the deletes run against (required by AttestoPhoenix.Config).
  • :sweep_interval_ms - how often a sweep runs, in milliseconds. When this key is unset the sweeper MUST NOT be placed in the supervision tree; start_link/1 raises rather than silently choosing an interval, so a missing interval is a configuration error, not a default.
  • :table_prefix - optional Ecto schema/table prefix applied to every delete so a host that installed the generated tables under a non-default prefix sweeps the same tables it created.

The set of swept tables is fixed by the generated schema and is not host-configurable: every Ecto-backed store the library generates carries an expires_at column and is swept.

Summary

Functions

Starts the sweeper.

Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.

Functions

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the sweeper.

Requires a %AttestoPhoenix.Config{} under the :config key. The config's :sweep_interval_ms MUST be a positive integer; a missing or non-positive interval raises ArgumentError so a misconfigured host fails at boot instead of starting a process that never sweeps.

sweep_now(server \\ __MODULE__)

@spec sweep_now(GenServer.server()) :: %{optional(String.t()) => non_neg_integer()}

Runs a single sweep synchronously and returns the number of rows deleted per table. Test- and diagnostic-facing; the supervised process drives sweeps via the configured interval, not this call.