Skip to content

fix(metering): durable exactly-once receipt for meter/grace; document the no-budget contract (#92, #99)#117

Merged
JumpTechCode merged 1 commit into
mainfrom
fix/metering-durable-receipt
Jul 20, 2026
Merged

fix(metering): durable exactly-once receipt for meter/grace; document the no-budget contract (#92, #99)#117
JumpTechCode merged 1 commit into
mainfrom
fix/metering-durable-receipt

Conversation

@JumpTechCode

Copy link
Copy Markdown
Contributor

#92 — meter/grace double-apply spend after the idempotency-key TTL

reserve is guarded forever by the reservation UNIQUE(principal_id, idempotency_key); commit/cancel by the reservation status machine. But meter and grace-backfill apply spend with no reservation, so the reaped idempotency_key row was their only dedup — and the reaper deletes it at idempotency_ttl_hours (default 24h). A retry after that finds no row → FRESHapply_spend runs a second time and double-books committed/overage on every node. Because period_start is recomputed at meter time, a cross-month retry books the duplicate in the wrong period.

Fix: a durable, never-reaped metered_receipt table with the same shape and claim/replay/mismatch semantics as idempotency_key. The idempotency repo is parameterized by table, so the same mechanism serves both the reaped table (reserve/commit/cancel — whose durable dedup lives elsewhere) and the permanent one (meter/grace). CommandContext exposes it as metered_receipt, and both handlers dedup through it. The receipt persists for the life of the record (like a reservation row), so:

  • exactly-once holds beyond any retry window (no double-apply past the TTL),
  • exact replay is preserved (the stored response is returned), and
  • key-reuse is still caught by fingerprint (MISMATCH → 409).

Migration 0006 is additive — a CREATE TABLE, no existing table or data touched. The idempotency reaper is unchanged (it only targets idempotency_key), so the receipt is never reaped.

#99 — no-budget metering returns 403 and drops spend (decision: keep 403, document)

A completed call governed by no budget is refused (403 budget_not_found, per ADR 0020's default-deny on an empty applicable set) rather than booked. This is intended for V1: metering needs a governing budget to attribute against, and "never deny" means "never deny on headroom," not "accept ungoverned spend." Documented explicitly in ADR 0037, with a synthesized/unattributed-node alternative noted as future work.

Tests

  • Integration proof (meter / grace-backfill have no durable exactly-once backstop; a retry after the key TTL double-applies spend #92): meter a call → assert the dedup lives in metered_receipt (not idempotency_key) → simulate the reaper (DELETE FROM idempotency_key) → retry → exact replay, spend applied once, 2 ledger rows (would double to 4 on the old code).
  • Meter/grace unit tests now run against metered_receipt, with an idempotency tripwire stub so any regression to the reaped store fails loudly.
  • Updated: the integration truncation list, the schema table-set assertion, and every CommandContext fake for the new member.

Local gate: ruff, mypy --strict, import-linter, full unit suite (492), full integration suite (180), schema-migration drift check — all green.

Closes #92, #99.

… the no-budget contract (#92, #99)

#92: meter and grace-backfill apply spend with no reservation, so the reaped
idempotency_key row was their ONLY dedup — a retry after its TTL (default 24h)
re-ran apply_spend and double-booked the spend, possibly in the wrong period
(period_start is recomputed at meter time). reserve is guarded forever by the
reservation UNIQUE; commit/cancel by the reservation status machine; meter/grace
had no such durable backstop.

Adds a never-reaped `metered_receipt` table with the same shape and
claim/replay/mismatch semantics as `idempotency_key`. The idempotency repo is
parameterized by table so the identical mechanism serves both the reaped table
(reserve/commit/cancel) and the permanent one (meter/grace); the CommandContext
exposes it as `metered_receipt`, and both handlers dedup through it. The receipt
persists for the life of the record (like a reservation row), so exactly-once
holds beyond any retry window, exact replay is preserved, and key-reuse is still
caught by fingerprint. Additive migration 0006 (no existing table/data touched).

#99 (decision: keep 403, document the contract): a completed call governed by no
budget is refused (403 budget_not_found, ADR 0020 default-deny) rather than
booked — intended for V1, since metering needs a governing budget to attribute
against and "never deny" means "never deny on headroom." Documented in ADR 0037,
with a synthesized/unattributed-node alternative noted as future work.

Tests: an integration proof that meter dedup survives the idempotency-key reaper
(delete idempotency_key, retry -> exact replay, no double-apply); meter/grace
unit tests now run against metered_receipt with an idempotency tripwire so a
regression to the reaped store is caught; conftest truncation, schema table set,
and all CommandContext fakes updated for the new member.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BjJ4QScUKbrMzF1jujL5Bg
@JumpTechCode
JumpTechCode merged commit b851c74 into main Jul 20, 2026
8 checks passed
@JumpTechCode
JumpTechCode deleted the fix/metering-durable-receipt branch July 20, 2026 21:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

meter / grace-backfill have no durable exactly-once backstop; a retry after the key TTL double-applies spend

1 participant