gendesign/tradein-mvp/backend/tests/test_yandex_cian_backfill_cadence.py
bot-backend 4ddb3c7196
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI / changes (pull_request) Successful in 12s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-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 4m58s
chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал
compute_next_run_at держит суточную гранулярность (interval_days, минимум 1),
подчасовой такт делается хуком reschedule_after_minutes как post_claim. У
avito_detail_backfill он есть (180 мин), у yandex_detail_backfill и
cian_detail_backfill не было — отсюда один прогон в сутки.

Цена простоя по замеру прода 31.08:

  Яндекс: 375-450 карточек за прогон, блоков НОЛЬ за неделю, очередь 11110
          → 25 суток при нынешнем такте
  Циан:   блоков ноль, очередь 20501

Такты разные, и это не произвол:

  yandex — 180 мин (8 прогонов/сутки). Ходит через resolve_proxy_url: берёт
           URL узла, но НЕ лизует его, поэтому чужие прогоны не блокирует.
  cian   — 360 мин (4 прогона/сутки). Ходит через BrowserFetcher и ДЕРЖИТ
           lease весь прогон, то есть отнимает узел у Авито и Домклика. Пул
           дефицитен (#2638), поэтому осторожнее.

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

283_scrape_schedules_cadence_yandex_cian_detail.sql — идемпотентный
UPDATE ... SET default_params = default_params || jsonb, остальные ключи
параметров не трогает.

Значения 180/360 — консервативная отправная точка по аналогии с Авито, а не
найденный оптимум: двигать вниз только по замеру нескольких суток, глядя и на
свипы тоже (тот же довод, что в комментарии у avito_detail_backfill).

Тесты (5): наличие post_claim у обоих, дефолтные интервалы, переопределение
через params. Прогон: 397 passed, 1 skipped, ruff чист.
2026-08-31 14:17:12 +03:00

104 lines
4.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.

"""Каденс yandex_detail_backfill и cian_detail_backfill: минуты, а не сутки.
Замер прода 31.08.2026: та же дыра, что чинили для avito_detail_backfill
(`test_avito_backfill_cadence.py`) — `compute_next_run_at` держит суточную
гранулярность (`interval_days = max(1, ...)`), а оба хендлера были
зарегистрированы без `post_claim`. Прогон Яндекса разбирает 375-450 карточек
за час при очереди 11 110, Циан — ~23 с/карточку при очереди 20 501, и оба
простаивали 23 часа из 24 при нуле блоков за неделю.
Тесты закрепляют именно подключённость хука и разные дефолты (180 у Яндекса,
360 у Циана) — если кто-то выровняет их значения без замера, тест на дефолт
это не поймает, но факт подключения и разность значений — да. Асимметрия
интервалов имеет причину (Циан держит lease узла весь прогон, Яндекс — нет),
см. комментарий в product_handlers.py у cian_detail_backfill.
"""
from __future__ import annotations
from unittest.mock import MagicMock
from app.services.product_handlers import build_product_handlers
class _DB:
"""Минимальный double для post_claim-хука: перехватывает mins из execute()."""
def __init__(self) -> None:
self.captured: dict[str, int] = {}
def execute(self, _stmt: object, params: dict[str, object]) -> None:
self.captured["mins"] = int(params["mins"]) # type: ignore[arg-type]
def commit(self) -> None:
return None
def test_yandex_detail_backfill_has_sub_daily_cadence_hook() -> None:
"""Без post_claim yandex_detail_backfill откатывается к суточной гранулярности."""
handlers = build_product_handlers(MagicMock())
handler = handlers["yandex_detail_backfill"]
assert handler.post_claim is not None, (
"yandex_detail_backfill остался без post_claim — каденс вернулся к "
"одному прогону в сутки (compute_next_run_at, interval_days >= 1)"
)
def test_cian_detail_backfill_has_sub_daily_cadence_hook() -> None:
"""Без post_claim cian_detail_backfill откатывается к суточной гранулярности."""
handlers = build_product_handlers(MagicMock())
handler = handlers["cian_detail_backfill"]
assert handler.post_claim is not None, (
"cian_detail_backfill остался без post_claim — каденс вернулся к "
"одному прогону в сутки (compute_next_run_at, interval_days >= 1)"
)
def test_yandex_default_interval_is_180_minutes() -> None:
"""Яндекс не держит lease узла (resolve_proxy_url) — можно 8 прогонов/сутки."""
handlers = build_product_handlers(MagicMock())
hook = handlers["yandex_detail_backfill"].post_claim
assert hook is not None
db = _DB()
hook(db, 1, {}, MagicMock())
assert db.captured["mins"] == 180
def test_cian_default_interval_is_360_minutes_not_180() -> None:
"""Циан держит lease узла (BrowserFetcher) весь прогон — вдвое реже Яндекса.
Явно проверяем НЕравенство 180: выравнивание интервалов с Яндексом отняло
бы дефицитный узел (3 живых на пул) у avito/domclick_detail_backfill без
замера последствий.
"""
handlers = build_product_handlers(MagicMock())
hook = handlers["cian_detail_backfill"].post_claim
assert hook is not None
db = _DB()
hook(db, 1, {}, MagicMock())
assert db.captured["mins"] == 360
assert db.captured["mins"] != 180
def test_interval_minutes_from_params_wins_for_both() -> None:
"""Подбор интервала — через default_params расписания, без правки кода."""
handlers = build_product_handlers(MagicMock())
yandex_hook = handlers["yandex_detail_backfill"].post_claim
cian_hook = handlers["cian_detail_backfill"].post_claim
assert yandex_hook is not None
assert cian_hook is not None
db_yandex = _DB()
yandex_hook(db_yandex, 1, {"interval_minutes": 90}, MagicMock())
assert db_yandex.captured["mins"] == 90
db_cian = _DB()
cian_hook(db_cian, 1, {"interval_minutes": 240}, MagicMock())
assert db_cian.captured["mins"] == 240