Merge pull request 'fix(tradein/avito): бэкфилл простаивал 23 часа из 24 — каденс был суточным' (#3054) from fix/avito-backfill-cadence into main
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m52s
Deploy Trade-In / build-backend (push) Successful in 1m21s
Deploy Trade-In / deploy (push) Successful in 1m57s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
All checks were successful
Deploy Trade-In / changes (push) Successful in 10s
Deploy Trade-In / build-frontend (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / test (push) Successful in 3m52s
Deploy Trade-In / build-backend (push) Successful in 1m21s
Deploy Trade-In / deploy (push) Successful in 1m57s
Deploy Trade-In / deploy-status (push) Successful in 1s
Deploy Trade-In / perimeter-smoke (push) Successful in 10s
This commit is contained in:
commit
b5b7a8246e
2 changed files with 103 additions and 1 deletions
|
|
@ -656,7 +656,30 @@ def build_product_handlers(ctx: SchedulerContext) -> dict[str, Handler]:
|
||||||
"geocode_missing_listings": Handler(
|
"geocode_missing_listings": Handler(
|
||||||
_job_geocode_missing_listings, "geocode_missing_listings"
|
_job_geocode_missing_listings, "geocode_missing_listings"
|
||||||
),
|
),
|
||||||
"avito_detail_backfill": Handler(_job_avito_detail_backfill, "avito_detail_backfill"),
|
# Каденс — в минутах, а не в сутках (замер 2026-08-22). Дефолтная
|
||||||
|
# гранулярность compute_next_run_at — сутки, и бэкфилл получал ровно один
|
||||||
|
# прогон в день. При этом прогон умирает по бану через 17-83 минуты, то
|
||||||
|
# есть 23 часа из 24 задание простаивало: 238 обогащённых карточек за
|
||||||
|
# сутки при 9 951 активном объявлении — очередь разбиралась бы месяцами.
|
||||||
|
#
|
||||||
|
# Хук сам себя throttl'ит: пока прогон идёт, has_running_run в _claim_run
|
||||||
|
# возвращает None и next_run_at не сбрасывается.
|
||||||
|
#
|
||||||
|
# 180 минут — ОСОЗНАННО консервативная отправная точка, а не найденный
|
||||||
|
# оптимум. Данных для подбора нет: два прогона дали противоречивую
|
||||||
|
# картину (4562 — 83 мин и 175 карточек; 4586 через 4.3 часа, когда пул
|
||||||
|
# прокси был давно чист, — 17 мин и 42 карточки). Значит память Авито
|
||||||
|
# длиннее часов, и учащение может ухудшить выход, а не улучшить.
|
||||||
|
#
|
||||||
|
# Риск, который надо держать в голове при подборе: те же 4 прокси
|
||||||
|
# обслуживают SERP-свипы — первичный сбор. Сжечь их на обогащении хуже,
|
||||||
|
# чем медленно обогащать. Двигать интервал вниз только по замеру
|
||||||
|
# нескольких суток, глядя и на свипы тоже.
|
||||||
|
"avito_detail_backfill": Handler(
|
||||||
|
_job_avito_detail_backfill,
|
||||||
|
"avito_detail_backfill",
|
||||||
|
post_claim=reschedule_after_minutes(param="interval_minutes", default=180),
|
||||||
|
),
|
||||||
"yandex_detail_backfill": Handler(_job_yandex_detail_backfill, "yandex_detail_backfill"),
|
"yandex_detail_backfill": Handler(_job_yandex_detail_backfill, "yandex_detail_backfill"),
|
||||||
"domclick_detail_backfill": Handler(
|
"domclick_detail_backfill": Handler(
|
||||||
_job_domclick_detail_backfill, "domclick_detail_backfill"
|
_job_domclick_detail_backfill, "domclick_detail_backfill"
|
||||||
|
|
|
||||||
79
tradein-mvp/backend/tests/test_avito_backfill_cadence.py
Normal file
79
tradein-mvp/backend/tests/test_avito_backfill_cadence.py
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
"""Каденс 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
|
||||||
Loading…
Add table
Reference in a new issue