chore(tradein/scheduler): добор карточек Яндекса и Циана шёл раз в сутки и простаивал
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
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 чист.
This commit is contained in:
parent
c4fcedda9a
commit
4ddb3c7196
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