fix(tradein/geocoder): темп Nominatim сдерживается перед запросом, а не после успеха (#2953) #2954

Merged
bot-backend merged 1 commit from fix/2953-nominatim-throttle into main 2026-08-20 07:55:42 +00:00
Collaborator

Закрывает #2953.

Что было

194 события 429 Too many requests от Nominatim за неделю. Политику 1 req/sec изображали три asyncio.sleep(1.0), и ни один не ограничивал:

место что делал
geocode() сон внутри if result is not None — только после успеха
_nominatim_lookup() сон между typo-вариантами, но не перед tier-1
цикл бэкфилла своей паузы не имеет вовсе

Для неразрешимого адреса: tier-1 в t=0, четыре варианта к t=4 — 5 запросов за 4 секунды, дальше сразу следующий адрес без паузы. Темп сбрасывался ровно тогда, когда сервер просит притормозить.

Почему это происходит каждый прогон, а не изредка

Разбор 16 947 объявлений без координат (20.08):

без координат всего 16 947
активных 1 948
активных с пригодным адресом 1 471
доступно бэкфиллу сейчас 47
в семидневном backoff 1 424

97 % активных «без координат» — повторные неудачи, возвращающиеся в очередь каждые 7 суток. Череда неудач — норма прогона, а не исключение.

Правка

Один общий ограничитель, пауза перед каждым обращением, одна на все три точки вызова (/search ×2, /reverse). Четыре прежних sleep убраны как избыточные — код стал короче.

Повторы на 429 намеренно не трогал. Если убрать их той же правкой, падение счётчика 429 нельзя будет приписать именно ограничителю — а критерий приёмки в #2953 сформулирован через этот счётчик. Если 429 останутся, повторы — следующий шаг.

ponytail: ограничитель внутрипроцессный. Если бэкфилл и пользовательские запросы разъедут по разным процессам, темпы снова сложатся — тогда понадобится общий счётчик (Redis). Ceiling записан комментарием в коде.

Проверка — двусторонняя, по поведению

Тест меряет расстояние между исходящими запросами, а не наличие нового символа: from … import _nominatim_throttle дал бы на origin/main ImportError, то есть «возможности нет», и красный ничего бы не доказывал.

Против кода origin/main:

зазоры между тремя запросами   [0.0003, 0.0001]  вместо ≥0.08   → падает
стык двух неудачных поисков    0.0022с           вместо ≥0.08   → падает
первый запрос не ждёт впустую  контроль — зелёный на обеих сторонах

Третий тест — контроль в обе стороны: он же сломается, если интервал по недосмотру начнёт применяться дважды за запрос или если «время последнего вызова» инициализируют текущим временем (тогда каждый холодный старт платил бы интервал ни за что).

Прогоны

tests/test_geocod*.py + reverse + новый     303 passed   rc=0
tradein-сьют (с правкой)   280 failed / 2099 passed / 216 errors
tradein-сьют (база main)   280 failed / 2096 passed / 216 errors

Ни одного нового падения, +3 своих. Падения и collection-errors средовые — в локальном venv нет scraper_kit/bcrypt/lxml; проверено подстановкой файла с origin/main и повторным прогоном.

Закрывает #2953. ## Что было 194 события `429 Too many requests` от Nominatim за неделю. Политику 1 req/sec изображали три `asyncio.sleep(1.0)`, и ни один не ограничивал: | место | что делал | |---|---| | `geocode()` | сон **внутри** `if result is not None` — только после успеха | | `_nominatim_lookup()` | сон **между** typo-вариантами, но не перед tier-1 | | цикл бэкфилла | своей паузы не имеет вовсе | Для неразрешимого адреса: tier-1 в t=0, четыре варианта к t=4 — **5 запросов за 4 секунды**, дальше сразу следующий адрес без паузы. Темп сбрасывался ровно тогда, когда сервер просит притормозить. ## Почему это происходит каждый прогон, а не изредка Разбор 16 947 объявлений без координат (20.08): | | | |---|---| | без координат всего | 16 947 | | активных | 1 948 | | активных с пригодным адресом | 1 471 | | доступно бэкфиллу сейчас | 47 | | **в семидневном backoff** | **1 424** | 97 % активных «без координат» — повторные неудачи, возвращающиеся в очередь каждые 7 суток. Череда неудач — норма прогона, а не исключение. ## Правка Один общий ограничитель, пауза **перед** каждым обращением, одна на все три точки вызова (`/search` ×2, `/reverse`). Четыре прежних `sleep` убраны как избыточные — код стал короче. Повторы на `429` намеренно **не трогал**. Если убрать их той же правкой, падение счётчика 429 нельзя будет приписать именно ограничителю — а критерий приёмки в #2953 сформулирован через этот счётчик. Если 429 останутся, повторы — следующий шаг. `ponytail`: ограничитель внутрипроцессный. Если бэкфилл и пользовательские запросы разъедут по разным процессам, темпы снова сложатся — тогда понадобится общий счётчик (Redis). Ceiling записан комментарием в коде. ## Проверка — двусторонняя, по поведению Тест меряет **расстояние между исходящими запросами**, а не наличие нового символа: `from … import _nominatim_throttle` дал бы на `origin/main` `ImportError`, то есть «возможности нет», и красный ничего бы не доказывал. Против кода `origin/main`: ``` зазоры между тремя запросами [0.0003, 0.0001] вместо ≥0.08 → падает стык двух неудачных поисков 0.0022с вместо ≥0.08 → падает первый запрос не ждёт впустую контроль — зелёный на обеих сторонах ``` Третий тест — контроль в обе стороны: он же сломается, если интервал по недосмотру начнёт применяться дважды за запрос или если «время последнего вызова» инициализируют текущим временем (тогда каждый холодный старт платил бы интервал ни за что). ## Прогоны ``` tests/test_geocod*.py + reverse + новый 303 passed rc=0 tradein-сьют (с правкой) 280 failed / 2099 passed / 216 errors tradein-сьют (база main) 280 failed / 2096 passed / 216 errors ``` Ни одного нового падения, +3 своих. Падения и collection-errors средовые — в локальном venv нет `scraper_kit`/`bcrypt`/`lxml`; проверено подстановкой файла с `origin/main` и повторным прогоном.
bot-backend added 1 commit 2026-08-20 07:47:13 +00:00
fix(tradein/geocoder): темп Nominatim сдерживается перед запросом, а не после успеха (#2953)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 9s
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 4m44s
5222f477db
На проде — 194 события «429 Too many requests» от Nominatim за неделю. Это не
внешняя случайность: политику 1 req/sec изображали три разрозненных
asyncio.sleep(1.0), и ни один не давал ограничения на самом деле.

  geocode()           — сон ВНУТРИ `if result is not None`, то есть только
                        после УСПЕХА; после неудачи паузы не было;
  _nominatim_lookup() — сон МЕЖДУ typo-вариантами, но не перед tier-1;
  цикл бэкфилла       — своей паузы не имеет вовсе, зовёт geocode()
                        последовательно.

Для неразрешимого адреса выходило 5 запросов за 4 секунды и сразу следующий
адрес без паузы. То есть темп сбрасывался ровно тогда, когда сервер и просит
притормозить, и разгонялся ровно на тех адресах, что не резолвятся.

А череда неудач — не редкость, а норма прогона. Разбор 16947 объявлений без
координат на 20.08: активных 1948, из них с пригодным адресом 1471, доступно
бэкфиллу прямо сейчас 47, а 1424 лежат в семидневном backoff — то есть 97%
активных «без координат» это повторные неудачи, возвращающиеся в очередь
каждые 7 суток. Разгон происходит каждый прогон.

Теперь пауза берётся ПЕРЕД каждым обращением и одна на все три точки вызова
(/search x2, /reverse). Четыре прежних sleep убраны как избыточные.

Повторы на 429 намеренно НЕ трогал. Если убрать их той же правкой, падение
счётчика 429 нельзя будет приписать именно ограничителю темпа — а критерий
приёмки в #2953 сформулирован через этот счётчик.

Тест проверяет ПОВЕДЕНИЕ (расстояние между исходящими запросами), а не наличие
нового символа: импорт _nominatim_throttle дал бы на origin/main ImportError,
то есть «возможности нет», и красный ничего бы не доказывал. Против кода
origin/main:

  зазоры между тремя запросами   [0.0003, 0.0001] вместо >=0.08  -> падает
  стык двух неудачных поисков    0.0022с          вместо >=0.08  -> падает
  первый запрос не ждёт впустую  контроль, зелёный на обеих сторонах

Прогон tradein-сьюта: 280 failed / 2099 passed против базы 280 failed /
2096 passed на origin/main — ни одного нового падения, +3 своих. Падения и
216 collection-errors средовые (нет scraper_kit/bcrypt/lxml в локальном venv).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bot-backend merged commit 3b40ba09f7 into main 2026-08-20 07:55:42 +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#2954
No description provided.