check_and_raise (SELECT) and increment (unconditional UPSERT) were 5-40s apart (the estimate scrapes run between them), so two concurrent /estimate at used=14 both passed the check and both incremented → used=16, pilot exceeds 15/mo. Make increment atomic-conditional: ON CONFLICT DO UPDATE SET used=used+1 WHERE used < :lim RETURNING used → returns bool. The WHERE applies only to the DO UPDATE branch, so a fresh insert (used=1) always succeeds; a conflicting row already at the limit is not updated → RETURNING empty → False. trade_in raises 429 when increment returns False. check_and_raise kept as a fast pre-check. Tests: 4 new (atomic refuse-at-limit incl. used stays == limit not +2; fresh insert succeeds; returns bool; unlimited no-op) via an in-memory fake modeling the conditional UPSERT (CI has no Postgres). pytest -k quota → 36 passed. Refs #747 |
||
|---|---|---|
| .. | ||
| api | ||
| core | ||
| observability | ||
| schemas | ||
| services | ||
| tasks | ||
| __init__.py | ||
| main.py | ||