feat(tradein/estimate): внешние оценки не ждутся в запросе — 9 секунд превращаются в 1 #3055

Merged
lekss361 merged 1 commit from feat/estimate-external-sources-background into main 2026-08-22 12:58:04 +00:00
Owner

Подготовка к публикации в РБК 30.08.

Замер, с которого всё началось

Расчёт по новому адресу — 8-12.4 с. По уже виденному (кэш 24 ч) — 0.4-0.8 с. Разбивка одного запроса по логам прода:

10.659  старт
10.827  дом найден              0.17 с
11.565  аналоги, 49 кандидатов  0.74 с
11.579  ДКП-коридор             0.01 с
17.576  yandex_valuation        ← 6.0 с
19.159  cian_valuation          ← 1.5 с
19.252  готово                  итого 8.6 с

Семь с половиной секунд из восьми с половиной — ожидание чужих HTTP. Наша база и сам расчёт укладываются в секунду. Геокодинг ни при чём: с готовыми координатами те же 7.5-10.8 с.

Публикация приведёт аудиторию на новые адреса, то есть мимо суточного кэша. Масштабирование контейнеров тут не помогает — время уходит на ожидание чужого ответа, а не на наши вычисления.

Статья, к слову, о том, как медленная оценка срывает сделки.

Что сделано

У обоих источников появился режим «только кэш» (fetch_on_miss). При ESTIMATE_EXTERNAL_SOURCES_BACKGROUND=true запрос делает лишь чтение кэша — локальный запрос на миллисекунды, — а свежая загрузка уходит в фон и наполняет кэш к следующему обращению по тому же адресу.

Почему это безопасно. Деградация источника в None — не новое состояние ответа: ровно так же ведёт себя таймаут estimate_*_valuation_timeout_s, и этот путь работает в проде сегодня. Контракт API не меняется, фронт уже умеет отсутствие этих полей.

Две ловушки, найденные при реализации

Наивный asyncio.gather двух источников небезопасен. Обе функции принимают один и тот же db и делают внутри db.commit() — коммит одной зафиксировал бы незавершённую работу другой. По этой же причине фоновая задача открывает свою сессию: сессия запроса закрывается вместе с ответом.

Неограниченный create_task превратил бы ускорение в отказ. Всплеск по новым адресам — ровно тот случай, ради которого режим и сделан — породил бы сотни параллельных задач с сессиями и HTTP-клиентами при max_connections=100 и mem_limit: 768m у backend. Очередь ограничена восемью; переполнение не ошибка, следующий запрос попробует снова.

Чего намеренно НЕ сделано

Снижение таймаутов до 4 с — предлагалось, отменено замером. Свежий запрос к Яндексу занимает 6 с; таймаут 4 обрывал бы его почти всегда, кэш бы не наполнялся, и источник оказался бы тихо отключён. Таймаут здесь страховка от патологии, а не регулятор задержки.

Параллельный запуск двух источников — даёт полторы секунды из восьми с половиной (общее время стало бы максимумом из 6 и 1.5). Имеет смысл как отдельная правка ради худшего случая (16 с → 8 с), но не как способ ускорить обычный запрос.

Проверка

Семь тестов: режим «только кэш» не ходит в сеть, дефолт по-прежнему ходит, фоновая задача берёт свою сессию и закрывает её, гасит ошибки, не падает без event loop, удерживает ссылку на задачу, очередь ограничена.

Фальсифицированы: на неизменённом коде падают 5 из 7 — проходит только сторож неизменности дефолта, что и правильно.

Смежные тесты оценщика зелёные: 29 штук (бюджет ЦИАН, интеграция ЦИАН, клиентские координаты, аудит).

Включение

Дефолт в коде False — другие окружения не меняются. На проде включено через docker-compose.prod.yml у сервиса backend (это tradein-backend, uvicorn с эндпоинтом оценки).

После мержа

Замерить на проде тот же новый адрес и убедиться, что ответ ~1 с, а в логах появилась отложенная догрузка. Затем нагрузочный прогон по нашему пути с прогретым кэшем — бить чужие API пачкой с боевого IP не нужно.

Подготовка к публикации в РБК 30.08. ## Замер, с которого всё началось Расчёт по **новому** адресу — 8-12.4 с. По уже виденному (кэш 24 ч) — 0.4-0.8 с. Разбивка одного запроса по логам прода: ``` 10.659 старт 10.827 дом найден 0.17 с 11.565 аналоги, 49 кандидатов 0.74 с 11.579 ДКП-коридор 0.01 с 17.576 yandex_valuation ← 6.0 с 19.159 cian_valuation ← 1.5 с 19.252 готово итого 8.6 с ``` **Семь с половиной секунд из восьми с половиной — ожидание чужих HTTP.** Наша база и сам расчёт укладываются в секунду. Геокодинг ни при чём: с готовыми координатами те же 7.5-10.8 с. Публикация приведёт аудиторию на новые адреса, то есть мимо суточного кэша. Масштабирование контейнеров тут не помогает — время уходит на ожидание чужого ответа, а не на наши вычисления. Статья, к слову, о том, как медленная оценка срывает сделки. ## Что сделано У обоих источников появился режим «только кэш» (`fetch_on_miss`). При `ESTIMATE_EXTERNAL_SOURCES_BACKGROUND=true` запрос делает лишь чтение кэша — локальный запрос на миллисекунды, — а свежая загрузка уходит в фон и наполняет кэш к следующему обращению по тому же адресу. **Почему это безопасно.** Деградация источника в `None` — не новое состояние ответа: ровно так же ведёт себя таймаут `estimate_*_valuation_timeout_s`, и этот путь работает в проде сегодня. Контракт API не меняется, фронт уже умеет отсутствие этих полей. ## Две ловушки, найденные при реализации **Наивный `asyncio.gather` двух источников небезопасен.** Обе функции принимают один и тот же `db` и делают внутри `db.commit()` — коммит одной зафиксировал бы незавершённую работу другой. По этой же причине фоновая задача открывает **свою** сессию: сессия запроса закрывается вместе с ответом. **Неограниченный `create_task` превратил бы ускорение в отказ.** Всплеск по новым адресам — ровно тот случай, ради которого режим и сделан — породил бы сотни параллельных задач с сессиями и HTTP-клиентами при `max_connections=100` и `mem_limit: 768m` у backend. Очередь ограничена восемью; переполнение не ошибка, следующий запрос попробует снова. ## Чего намеренно НЕ сделано **Снижение таймаутов до 4 с** — предлагалось, отменено замером. Свежий запрос к Яндексу занимает 6 с; таймаут 4 обрывал бы его почти всегда, кэш бы не наполнялся, и источник оказался бы **тихо отключён**. Таймаут здесь страховка от патологии, а не регулятор задержки. **Параллельный запуск двух источников** — даёт полторы секунды из восьми с половиной (общее время стало бы максимумом из 6 и 1.5). Имеет смысл как отдельная правка ради худшего случая (16 с → 8 с), но не как способ ускорить обычный запрос. ## Проверка Семь тестов: режим «только кэш» не ходит в сеть, дефолт по-прежнему ходит, фоновая задача берёт свою сессию и закрывает её, гасит ошибки, не падает без event loop, удерживает ссылку на задачу, очередь ограничена. Фальсифицированы: на неизменённом коде падают 5 из 7 — проходит только сторож неизменности дефолта, что и правильно. Смежные тесты оценщика зелёные: 29 штук (бюджет ЦИАН, интеграция ЦИАН, клиентские координаты, аудит). ## Включение Дефолт в коде `False` — другие окружения не меняются. На проде включено через `docker-compose.prod.yml` у сервиса `backend` (это `tradein-backend`, uvicorn с эндпоинтом оценки). ## После мержа Замерить на проде тот же новый адрес и убедиться, что ответ ~1 с, а в логах появилась отложенная догрузка. Затем нагрузочный прогон по нашему пути с прогретым кэшем — бить чужие API пачкой с боевого IP не нужно.
lekss361 added 1 commit 2026-08-22 12:52:10 +00:00
feat(tradein/estimate): внешние оценки не ждутся в запросе — 9 секунд превращаются в 1
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 4m42s
5b447ec33d
Замер на проде 2026-08-22, разбивка одного расчёта по логам:

    10.659  старт
    10.827  дом найден              0.17 с
    11.565  аналоги, 49 кандидатов  0.74 с
    11.579  ДКП-коридор             0.01 с
    17.576  yandex_valuation        ← 6.0 с
    19.159  cian_valuation          ← 1.5 с
    19.252  готово                  итого 8.6 с

Семь с половиной секунд из восьми с половиной — ожидание чужих HTTP. Наша база
и сам расчёт укладываются в секунду. По уже виденному адресу (кэш 24 ч) — 0.4-0.8 с,
по новому — 8-12.4 с. Геокодинг ни при чём: с готовыми координатами те же 7.5-10.8.

Публикация в РБК 30.08 приведёт аудиторию на НОВЫЕ адреса, то есть мимо кэша.
Масштабирование контейнеров тут не помогает: время уходит на ожидание чужого
ответа, а не на наши вычисления.

Что сделано: у обоих источников появился режим «только кэш» (fetch_on_miss).
При включённом ESTIMATE_EXTERNAL_SOURCES_BACKGROUND запрос делает лишь чтение
кэша (локальный запрос на миллисекунды), а свежая загрузка уходит в фон и
наполняет кэш к следующему обращению по тому же адресу.

Почему это безопасно: деградация источника в None — НЕ новое состояние ответа.
Ровно так же ведёт себя таймаут estimate_*_valuation_timeout_s, и этот путь
работает в проде сегодня. Контракт API не меняется.

Фоновая задача берёт СВОЮ сессию: сессия запроса закрывается вместе с ответом,
а обе функции источников делают внутри себя db.commit() — переиспользование
чужой сессии зафиксировало бы её незавершённую работу. По той же причине
отвергнут наивный asyncio.gather двух источников на одной сессии.

Очередь догрузки ограничена восемью задачами. Без потолка всплеск по новым
адресам — ровно тот случай, ради которого режим и сделан — породил бы сотни
параллельных задач с сессиями и HTTP-клиентами при max_connections 100 и
mem_limit 768m у backend, то есть отказ вместо ускорения.

Дефолт в коде False: поведение других окружений не меняется. На проде режим
включён через docker-compose.prod.yml у сервиса backend.

Отдельно НЕ сделано, хотя предлагалось: снижение таймаутов до 4 с. Замер
показал, что свежий запрос к Яндексу занимает 6 с — таймаут 4 обрывал бы его
почти всегда, кэш бы не наполнялся, и источник оказался бы тихо отключён.
Таймаут здесь страховка от патологии, а не регулятор задержки.

Тесты: 7 штук на режим «только кэш», собственную сессию, гашение ошибок,
удержание ссылки на задачу и потолок очереди. Фальсифицированы — на неизменённом
коде падают 5 из 7 (проходит только сторож неизменности дефолта). Смежные
тесты оценщика (29 штук: бюджет ЦИАН, клиентские координаты, аудит) зелёные.
lekss361 merged commit f5e2f5f5f3 into main 2026-08-22 12:58:04 +00:00
lekss361 deleted branch feat/estimate-external-sources-background 2026-08-22 12:58:04 +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#3055
No description provided.