gendesign/tradein-mvp/backend/tests/test_avito_backfill_cadence.py
bot-backend fa1399d59e
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 4m47s
fix(tradein/avito): бэкфилл простаивал 23 часа из 24 — каденс был суточным
Замер 2026-08-22, уже после починки транспорта (#3049): обогащение работает,
но очередь не разбирается. За сутки 238 карточек при 9 951 активном объявлении.

Причина в планировщике, а не в скрапинге. `compute_next_run_at` имеет суточную
гранулярность по построению — `interval_days = max(1, int(...))`, целевая дата
`now + interval_days`. Меньше суток не выражается. Бэкфилл при этом умирает по
бану через 17-83 минуты, то есть работал около часа в сутки, а остальное время
расписание ждало следующего дня.

Механизм sub-hourly каденса уже был написан — `reschedule_after_minutes`
(#2162, сделан для proxy_healthcheck). Его просто не подключили к бэкфиллу.
Хук ставит next_run_at = now() + interval_minutes сразу после claim и сам себя
тормозит: пока прогон идёт, has_running_run в _claim_run возвращает None.

180 минут — осознанно консервативная отправная точка, НЕ найденный оптимум.
Данных для подбора нет, и имеющиеся два прогона противоречат наивному
ожиданию: 4562 дал 175 карточек за 83 минуты, а 4586 через 4.3 часа — когда
пул прокси был давно чист — умер за 17 минут с 42 карточками. Значит память
Авито длиннее часов, и учащение может ухудшить выход.

Отдельно держать в голове при подборе: те же 4 прокси обслуживают SERP-свипы,
то есть первичный сбор. Сжечь их на обогащении хуже, чем медленно обогащать.
Двигать интервал вниз только по замеру нескольких суток, глядя и на свипы.
Подбор — через default_params расписания, правка кода для этого не нужна.

Тесты закрепляют подключённость хука и коридор дефолта, а не конкретное
значение: 180 будет двигаться, а вот утрата хука вернёт суточный простой
молча. Проверено фальсификацией — на неизменённом коде все три падают.
2026-08-22 14:57:20 +03:00

79 lines
3.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Каденс avito_detail_backfill: минуты, а не сутки.
Замер 2026-08-22 на проде: `compute_next_run_at` имеет суточную гранулярность
(`interval_days = max(1, int(...))`, целевая дата `now + interval_days`), поэтому
бэкфилл получал ровно один прогон в сутки. Прогон при этом умирает по бану через
17-83 минуты — 23 часа из 24 задание простаивало, 238 обогащённых карточек за
сутки при 9 951 активном объявлении.
Механизм sub-hourly каденса уже существовал (`reschedule_after_minutes`, #2162,
сделан для `proxy_healthcheck`) — не был подключён.
Тест закрепляет именно подключённость хука, а не значение интервала: 180 минут
подобраны консервативно и будут двигаться по замерам, а вот утрата хука вернёт
суточный простой молча.
"""
from __future__ import annotations
from unittest.mock import MagicMock
from app.services.product_handlers import build_product_handlers
def test_backfill_has_sub_daily_cadence_hook() -> None:
"""Без post_claim бэкфилл откатывается к суточной гранулярности."""
handlers = build_product_handlers(MagicMock())
handler = handlers["avito_detail_backfill"]
assert handler.post_claim is not None, (
"avito_detail_backfill остался без post_claim — каденс вернулся к "
"одному прогону в сутки (compute_next_run_at, interval_days >= 1)"
)
def test_backfill_default_interval_is_sane() -> None:
"""Дефолт не должен быть ни суточным, ни агрессивнее получаса.
Нижняя граница — не оптимум, а страховка: те же 4 прокси обслуживают
SERP-свипы, и слишком частое обогащение бьёт по первичному сбору.
"""
handlers = build_product_handlers(MagicMock())
hook = handlers["avito_detail_backfill"].post_claim
assert hook is not None
captured: dict[str, int] = {}
class _DB:
def execute(self, _stmt: object, params: dict[str, object]) -> None:
captured["mins"] = int(params["mins"]) # type: ignore[arg-type]
def commit(self) -> None:
return None
# params без ключа → сработает дефолт фабрики, его и проверяем.
hook(_DB(), 1, {}, MagicMock())
assert 30 <= captured["mins"] <= 360, (
f"интервал по умолчанию {captured['mins']} мин вне разумного коридора"
)
assert captured["mins"] < 1440, "интервал в сутки и больше — это исходный дефект"
def test_interval_minutes_from_params_wins() -> None:
"""Подбор интервала — через default_params расписания, без правки кода."""
handlers = build_product_handlers(MagicMock())
hook = handlers["avito_detail_backfill"].post_claim
assert hook is not None
captured: dict[str, int] = {}
class _DB:
def execute(self, _stmt: object, params: dict[str, object]) -> None:
captured["mins"] = int(params["mins"]) # type: ignore[arg-type]
def commit(self) -> None:
return None
hook(_DB(), 1, {"interval_minutes": 90}, MagicMock())
assert captured["mins"] == 90