fix: sweep expired confirmations, codes, and invites on the reaper #171

Merged
rosa merged 1 commit from ops/sweep-expired-capabilities into main 2026-08-10 02:50:45 +00:00
Owner

Closes #160.

Confirmations, authorization codes, and invites were deleted only when
consumed (keyed by identifier), so a capability that expired unredeemed was
never reclaimed and its table grew for the life of the deployment — the same
Steady-State violation the session reaper (#159) addressed, at lower volume.

What changed

  • AppRepository::delete_expired_capabilities(now) — one sweep across the
    three capability tables, returning the count removed.
  • SQLite adapter — three DELETEs. Confirmations and codes go on a plain
    expires_at < now (no retained terminal state — they're deleted on consume).
  • In-memory mock — mirrors the predicate via Invite::status(now) so the
    two adapters stay in agreement.
  • Background reaper (main.rs) — folded into the existing session-cleanup
    tick (session_reaperexpiry_reaper), so it drains on the same shutdown
    signal and runs whether or not background jobs are enabled.
  • Regenerated the .sqlx offline query cache for the three new queries.

Invite handling

Unlike confirmations and codes, a redeemed or revoked invite is a
deliberate historical record. A blanket expires_at < now would destroy it, so
the invite sweep is scoped to expires_at < ? AND redeemed_at IS NULL AND revoked_at IS NULL — exactly the rows Invite::status classifies as
Expired. This is the faithful reading of the issue's problem statement
("expired-but-never-redeemed"), narrower than its literal suggested fix.

Tests

A both_adapters! conformance suite asserts expired-unconsumed rows are
reclaimed, live rows and redeemed/revoked invites survive, and the returned
count is exact. Full suite, clippy, fmt, and ci all pass.

Closes #160. Confirmations, authorization codes, and invites were deleted only when consumed (keyed by `identifier`), so a capability that expired unredeemed was never reclaimed and its table grew for the life of the deployment — the same Steady-State violation the session reaper (#159) addressed, at lower volume. ## What changed - **`AppRepository::delete_expired_capabilities(now)`** — one sweep across the three capability tables, returning the count removed. - **SQLite adapter** — three `DELETE`s. Confirmations and codes go on a plain `expires_at < now` (no retained terminal state — they're deleted on consume). - **In-memory mock** — mirrors the predicate via `Invite::status(now)` so the two adapters stay in agreement. - **Background reaper** (`main.rs`) — folded into the existing session-cleanup tick (`session_reaper` → `expiry_reaper`), so it drains on the same shutdown signal and runs whether or not background jobs are enabled. - Regenerated the `.sqlx` offline query cache for the three new queries. ## Invite handling Unlike confirmations and codes, a **redeemed or revoked** invite is a deliberate historical record. A blanket `expires_at < now` would destroy it, so the invite sweep is scoped to `expires_at < ? AND redeemed_at IS NULL AND revoked_at IS NULL` — exactly the rows `Invite::status` classifies as `Expired`. This is the faithful reading of the issue's problem statement ("expired-but-never-redeemed"), narrower than its literal suggested fix. ## Tests A `both_adapters!` conformance suite asserts expired-unconsumed rows are reclaimed, live rows and redeemed/revoked invites survive, and the returned count is exact. Full suite, `clippy`, `fmt`, and `ci` all pass.
fix: sweep expired confirmations, codes, and invites on the reaper
All checks were successful
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
fb97e7a368
Confirmations, authorization codes, and invites were deleted only when
consumed (keyed by identifier), so a capability that expired unredeemed
was never reclaimed and its table grew for the life of the deployment —
the same Steady-State violation the session reaper (#159) addressed, at
lower volume.

Add AppRepository::delete_expired_capabilities, a single sweep across the
three tables, and fold it into the existing background reaper tick so it
drains on the same shutdown signal and runs whether or not background
jobs are enabled. Invites differ from the other two: a redeemed or
revoked invite is a deliberate record, so only invites still outstanding
at their expiry are swept — exactly what Invite::status calls Expired —
while confirmations and codes have no retained terminal state and go on a
plain expires_at cutoff.

Covered by a both-adapters conformance suite asserting expired-unconsumed
rows are reclaimed, live rows and redeemed/revoked invites survive, and
the returned count is exact.

Closes #160
rosa merged commit 90f56d5cca into main 2026-08-10 02:50:45 +00:00
rosa deleted branch ops/sweep-expired-capabilities 2026-08-10 02:50:45 +00:00
Sign in to join this conversation.
No description provided.