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
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 чист.
104 lines
4.7 KiB
Python
104 lines
4.7 KiB
Python
"""Каденс 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
|