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 |
||
|---|---|---|
| .. | ||
| app | ||
| data/sql | ||
| scripts | ||
| tests | ||
| .dockerignore | ||
| Dockerfile | ||
| pyproject.toml | ||