feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082) #3097

Merged
bot-backend merged 1 commit from fix/3082-estimate-concurrency into main 2026-08-26 07:36:38 +00:00
Collaborator

Что

На /trade-in/estimate не было ограничителя одновременности: рейт-лимит меряет частоту (300/60с вправе стартовать в одну секунду), квота — счётная и помесячная. Пила параллельных оценок (0.8–2.4с каждая, соединение общего пула + внешние тиры) выедала пул SQLAlchemy и тормозила весь /api/v1/* — детальный разбор в #3082.

Как

asyncio.Semaphore(4) по образцу public/mera.py::_suggest_slots (там 4 слота на секундное автодополнение, мотив тот же):

  • acquire ПОСЛЕ дешёвых отказов (рейт-лимит, квота pre-check) и ДО дорогой цепочки внешних вызовов;
  • ожидание слота 5с ≈ две длительности оценки, дальше — честный быстрый 429 + Retry-After, а не растущая очередь (очередь под нагрузкой = те же занятые соединения плюс таймаут у клиента);
  • release в finally сразу после дорогой части — инкремент квоты и сериализация ответа слот не держат.

4+4 слота (estimate+suggest) = 8 одновременно удерживаемых соединений из 15 пула (5+10), остаток — прочим ручкам.

Согласование с #3083

Семафор живёт в памяти процесса: при переходе на несколько воркеров uvicorn фактический лимит умножится на их число. Комментарий об этом стоит прямо у семафора; #3083 начинать с замера, как её текст и требует.

Из чеклиста #3082 сознательно НЕ здесь

  • asyncio.gather трёх внешних вызовов (IMV/Yandex/Cian): все три делят один SQLAlchemy Session, и часть работы уходит в asyncio.to_thread — параллелить без разбора всех трёх деревьев вызовов = ловушка session-poisoning (#2464). Отдельным коммитом после разбора.
  • ограничитель на v1/geocode.suggest_addresses — «отдельным решением» по тексту задачи.

Проверки

  • новый test_3082_estimate_concurrency_cap.py: 4 висящих оценки → 5-я получает быстрый 429 «занят» с Retry-After; после освобождения те же 4 завершаются 200 и следующий запрос проходит (release в finally). Красный на origin/main по ЗНАЧЕНИЮ (5-й прошёл с 200, 0×429) — не AttributeError: новые имена патчатся с raising=False.
  • сосед test_estimate_rate_limit.py нетронут: 5 passed суммарно.
  • в пределах лимита семафор прозрачен (инвариант-тест, зелёный в обеих эрах).

Приёмка на проде: N параллельных POST — сверх 4 быстрые 429; p95 остальных ручек не растёт линейно от числа оценок.

🤖 Generated with Claude Code

## Что На `/trade-in/estimate` не было ограничителя одновременности: рейт-лимит меряет частоту (300/60с вправе стартовать в одну секунду), квота — счётная и помесячная. Пила параллельных оценок (0.8–2.4с каждая, соединение общего пула + внешние тиры) выедала пул SQLAlchemy и тормозила весь `/api/v1/*` — детальный разбор в #3082. ## Как `asyncio.Semaphore(4)` по образцу `public/mera.py::_suggest_slots` (там 4 слота на секундное автодополнение, мотив тот же): - acquire ПОСЛЕ дешёвых отказов (рейт-лимит, квота pre-check) и ДО дорогой цепочки внешних вызовов; - ожидание слота 5с ≈ две длительности оценки, дальше — честный быстрый **429 + Retry-After**, а не растущая очередь (очередь под нагрузкой = те же занятые соединения плюс таймаут у клиента); - release в `finally` сразу после дорогой части — инкремент квоты и сериализация ответа слот не держат. 4+4 слота (estimate+suggest) = 8 одновременно удерживаемых соединений из 15 пула (5+10), остаток — прочим ручкам. ## Согласование с #3083 Семафор живёт в памяти процесса: при переходе на несколько воркеров uvicorn фактический лимит умножится на их число. Комментарий об этом стоит прямо у семафора; #3083 начинать с замера, как её текст и требует. ## Из чеклиста #3082 сознательно НЕ здесь - `asyncio.gather` трёх внешних вызовов (IMV/Yandex/Cian): все три делят один SQLAlchemy `Session`, и часть работы уходит в `asyncio.to_thread` — параллелить без разбора всех трёх деревьев вызовов = ловушка session-poisoning (#2464). Отдельным коммитом после разбора. - ограничитель на `v1/geocode.suggest_addresses` — «отдельным решением» по тексту задачи. ## Проверки - новый `test_3082_estimate_concurrency_cap.py`: 4 висящих оценки → 5-я получает быстрый 429 «занят» с Retry-After; после освобождения те же 4 завершаются 200 и следующий запрос проходит (release в finally). **Красный на origin/main по ЗНАЧЕНИЮ** (5-й прошёл с 200, 0×429) — не AttributeError: новые имена патчатся с raising=False. - сосед `test_estimate_rate_limit.py` нетронут: 5 passed суммарно. - в пределах лимита семафор прозрачен (инвариант-тест, зелёный в обеих эрах). Приёмка на проде: N параллельных POST — сверх 4 быстрые 429; p95 остальных ручек не растёт линейно от числа оценок. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bot-backend added 1 commit 2026-08-26 07:30:03 +00:00
feat(tradein/estimate): потолок одновременных оценок — 4 слота, быстрый 429 (#3082)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 11s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Successful in 4m41s
4b245b6605
Рейт-лимит меряет частоту (300/60с вправе стартовать в одну секунду), квота —
счётная и помесячная: параллелизм /estimate не ограничивал никто. Оценка
0.8–2.4с держит соединение общего пула SQLAlchemy (5+10) и внешние тиры —
пила одновременных оценок выедала пул и тормозила весь /api/v1/*.

Семафор по образцу public/mera.py::_suggest_slots: acquire после дешёвых
отказов (рейт-лимит, квота) с ожиданием 5с ≈ две длительности оценки,
timeout → 429 с Retry-After; release в finally сразу после дорогой части.
4+4 слота (estimate+suggest) = 8 удерживаемых соединений из 15 пула.

Семафор в памяти процесса — при переходе на несколько воркеров (#3083)
лимит умножится на их число; задачи согласовывать (о чём комментарий на месте).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 929eff11d7 into main 2026-08-26 07:36:38 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3097
No description provided.