chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал #3301
3 changed files with 178 additions and 1 deletions
|
|
@ -710,10 +710,24 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]:
|
|||
# строки расписания — у них свои окна, свой next_run_at и свой счётчик
|
||||
# прогонов; гонять оба режима под одним source нельзя, планировщик держит
|
||||
# на source ровно один активный прогон.
|
||||
# Каденс — в минутах (замер прода 31.08.2026): та же суточная дыра
|
||||
# compute_next_run_at, что у avito/yandex_detail_backfill — Циан держит
|
||||
# темп ~23 с/карточку при очереди 20 501 и НОЛЕ блоков за неделю.
|
||||
#
|
||||
# 360 минут (4 прогона/сутки), НЕ 180 как у Яндекса — АСИММЕТРИЯ ниже
|
||||
# ОБЯЗАТЕЛЬНА к прочтению перед тем, как выравнивать эти два интервала:
|
||||
# Циан ходит через BrowserFetcher и ДЕРЖИТ lease узла на ВЕСЬ прогон, а
|
||||
# не только берёт URL прокси (как Яндекс через resolve_proxy_url/
|
||||
# proxy_egress). Пул сейчас 3 живых узла — дефицитный ресурс, который
|
||||
# Циан на время прогона отбирает у avito_detail_backfill и
|
||||
# domclick_detail_backfill. Учащение Циана до 180 мин без замера того,
|
||||
# как это скажется на соседях по пулу, — прямой риск сжечь мощность,
|
||||
# которой и так не хватает на троих.
|
||||
"cian_detail_backfill": Handler(
|
||||
_job_cian_history_backfill,
|
||||
"cian_detail_backfill",
|
||||
pre_claim=_cian_pre_claim,
|
||||
post_claim=reschedule_after_minutes(param="interval_minutes", default=360),
|
||||
),
|
||||
"rosreestr_dkp_import": Handler(_job_rosreestr_dkp, "rosreestr_dkp_import"),
|
||||
"listing_source_snapshot": Handler(_job_listing_source_snapshot, "listing_source_snapshot"),
|
||||
|
|
@ -767,7 +781,21 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]:
|
|||
"avito_detail_backfill",
|
||||
post_claim=reschedule_after_minutes(param="interval_minutes", default=180),
|
||||
),
|
||||
"yandex_detail_backfill": Handler(_job_yandex_detail_backfill, "yandex_detail_backfill"),
|
||||
# Каденс — в минутах (замер прода 31.08.2026), тот же дефект, что у
|
||||
# avito_detail_backfill выше: без post_claim compute_next_run_at держит
|
||||
# суточную гранулярность, а прогон Яндекса разбирает 375-450 карточек в
|
||||
# час (budget_sec=3600) при очереди 11 110 и НУЛЕ блоков за неделю — узкое
|
||||
# место чисто в частоте запуска, не в самом фетчере.
|
||||
#
|
||||
# 180 минут (8 прогонов/сутки) безопасны именно из-за асимметрии с Циан:
|
||||
# Яндекс ходит через resolve_proxy_url (proxy_egress) — берёт URL прокси,
|
||||
# но НЕ держит lease узла на весь прогон, поэтому учащение не отнимает
|
||||
# узел у других source.
|
||||
"yandex_detail_backfill": Handler(
|
||||
_job_yandex_detail_backfill,
|
||||
"yandex_detail_backfill",
|
||||
post_claim=reschedule_after_minutes(param="interval_minutes", default=180),
|
||||
),
|
||||
"domclick_detail_backfill": Handler(
|
||||
_job_domclick_detail_backfill, "domclick_detail_backfill"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
-- 283_scrape_schedules_cadence_yandex_cian_detail.sql
|
||||
-- Раскрыть такт добора карточек Яндекса и Циана (замер прода 31.08.2026).
|
||||
--
|
||||
-- ЧТО БЫЛО НЕ ТАК
|
||||
-- compute_next_run_at держит суточную гранулярность (interval_days, минимум 1).
|
||||
-- yandex_detail_backfill и cian_detail_backfill были зарегистрированы БЕЗ
|
||||
-- post_claim-хука reschedule_after_minutes — тот же дефект, что чинили для
|
||||
-- avito_detail_backfill в #2162/миграции каденса выше по номерам. Итог:
|
||||
-- * Яндекс: прогон разбирает 375-450 карточек за час (budget_sec=3600),
|
||||
-- блоков НОЛЬ за неделю, очередь 11 110 — простаивало 23 часа из 24;
|
||||
-- * Циан: ~23 с/карточку, блоков НОЛЬ за неделю, очередь 20 501 — то же самое.
|
||||
-- Механизм подключён кодом (product_handlers.py) в этой же паре PR/коммитов;
|
||||
-- эта миграция прописывает "interval_minutes" в default_params, чтобы новый
|
||||
-- каденс был явным в расписании, а не только в дефолте хука.
|
||||
--
|
||||
-- ПОЧЕМУ РАЗНЫЕ ИНТЕРВАЛЫ (180 vs 360) — АСИММЕТРИЯ LEASE
|
||||
-- Яндекс ходит через resolve_proxy_url (proxy_egress) — берёт URL прокси, но
|
||||
-- НЕ держит lease узла на весь прогон, поэтому учащение не блокирует чужие
|
||||
-- прогоны. Отсюда 180 минут (8 прогонов/сутки).
|
||||
--
|
||||
-- Циан ходит через BrowserFetcher и ДЕРЖИТ lease узла на ВЕСЬ прогон, то есть
|
||||
-- на время прогона отнимает узел у avito_detail_backfill и domclick_detail_backfill.
|
||||
-- Пул сейчас 3 живых узла — дефицитный ресурс. Поэтому Циану 360 минут (4
|
||||
-- прогона/сутки), а НЕ 180: учащать дальше без замера влияния на соседей по
|
||||
-- пулу нельзя — риск сжечь мощность на троих.
|
||||
--
|
||||
-- ИДЕМПОТЕНТНОСТЬ
|
||||
-- `default_params || jsonb` — второй операнд перекрывает ключ "interval_minutes"
|
||||
-- при повторном прогоне, никаких дублей и никакого ON CONFLICT не требуется:
|
||||
-- это UPDATE существующих строк, а не INSERT.
|
||||
--
|
||||
-- ЗАВИСИМОСТИ: 052_scrape_schedules.sql (таблица), 282_*.sql (сама строка
|
||||
-- cian_detail_backfill уже существует к моменту этой миграции).
|
||||
|
||||
BEGIN;
|
||||
|
||||
UPDATE scrape_schedules
|
||||
SET default_params = default_params || '{"interval_minutes": 180}'::jsonb
|
||||
WHERE source = 'yandex_detail_backfill';
|
||||
|
||||
UPDATE scrape_schedules
|
||||
SET default_params = default_params || '{"interval_minutes": 360}'::jsonb
|
||||
WHERE source = 'cian_detail_backfill';
|
||||
|
||||
COMMIT;
|
||||
104
tradein-mvp/backend/tests/test_yandex_cian_backfill_cadence.py
Normal file
104
tradein-mvp/backend/tests/test_yandex_cian_backfill_cadence.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"""Каденс 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
|
||||
Loading…
Add table
Reference in a new issue