fix(tradein): atomic conditional quota increment — close TOCTOU (#747) #749
No reviewers
Labels
No labels
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
Fable 5 ревью
feedback/max
generative
GG-форсайт
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
вторичка
ИРД
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#749
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/747-quota-toctou"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
check_and_raise(SELECT) andincrement(unconditional UPSERT) are 5–40s apart — the estimate scrapes run between them. Two concurrent/estimateatused=14both pass the check (read 14<15) and both increment →used=16; the pilot exceeds the 15/mo gate.Fix (per the issue's exact contract)
incrementbecomes atomic-conditional and returnsbool:The
WHEREapplies only to theDO UPDATEbranch (PG semantics), so:used=1) always succeeds — first estimate of the month never blocked;RETURNINGempty →False.trade_in.pyraises429 LIMIT_EXHAUSTED_MESSAGEwhenincrementreturnsFalse.check_and_raiseis kept unchanged as the fast pre-check (429 before the expensive estimate) — but the atomic increment is now the source of truth.Out of scope (untouched per spec)
check_and_raise,get_status,is_unlimited, the limit/message, the frontend counter; no advisory lock.Test plan
grep 'used < :lim\|RETURNING used' account_quota.py→ 3;trade_in.pyguardsif not increment(...).pytest -k quota→ 36 passed (no regression), incl. 4 new: atused=MONTHLY_LIMIT-1the 2nd increment returnsFalseandused == limit(not +2); fresh insert succeeds; returnsbool; unlimited no-op. Atomic semantics modeled by an in-memory fake (CI has no Postgres).ruff checkclean.Refs #747
✅ APPROVE
Atomic conditional quota increment — закрытие TOCTOU (#747, demo-gate #659). PG-семантику проверил детально.
Correctness (
ON CONFLICT DO UPDATE … WHERE)fetchone()None → False. ✓trade_in.py: increment теперь source-of-truth (429 на False, после потраченной оценки — приемлемо, gate соблюдён);check_and_raiseсохранён как fast pre-check по спеку.None/unlimited → True no-op;LIMIT_EXHAUSTED_MESSAGE— валидный module-атрибут.Реализация дословно соответствует SQL-контракту issue. Нет миграции (существующая таблица), нет advisory-lock (верно — для single-row counter избыточно).
Tests — 4 новых с in-memory fake, моделирующим conditional-UPSERT (refuse-2nd-at-limit → used остаётся на лимите не +1, fresh insert, returns bool, unlimited no-op). 36 passed, ruff clean. Fake-вместо-реального-PG — принятое ограничение (нет CI Postgres); сам SQL — стандартный корректный паттерн, live concurrency → qa.
Scope/security — account_quota.py + trade_in.py + тест; нет secrets, нет self-extending триггера. Smoke → qa.