fix(avito): карточки берутся в свежем контексте браузера, а не в общем
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-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 5m1s
All checks were successful
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 13s
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-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 5m1s
Обогащение держало один контекст браузера на весь прогон (reuse_context=True). Авито закрывает карточки проверкой работой на JS, а её куки pow_challenge и pow_solved живут около пяти минут. Прогон живёт десятки минут, токен протухает и в живом контексте не обновляется, поэтому после первых карточек каждая следующая получает бан-страницу, и прогон рвёт защита по доле блокировок. Замер на проде 19.09, отдельная копия сайдкара, один и тот же прокси, 16 карточек на плечо, схема A-B-A: свежий контекст 14/16 и 13/16 успехов, 8.7 и 7.9 с на карточку общий контекст 6/16 успехов, 8.4 с на карточку Времени свежий контекст не стоит, а успешных карточек даёт вдвое больше. В плече с общим контекстом отказы нарастают по ходу: первые три успешны, последние пять подряд забанены. Переиспользование контекста пришло из #3118, где оно нужно ДомКлику, чтобы не терять куку его защиты. Для ДомКлика это остаётся верным, на Авито было перенесено по аналогии и замером опровергнуто. Поведение вынесено в AVITO_DETAIL_REUSE_CONTEXT с дефолтом false, чтобы вернуть старый режим без выката.
This commit is contained in:
parent
baed8f75a3
commit
65e44c9195
4 changed files with 183 additions and 33 deletions
|
|
@ -1066,6 +1066,26 @@ class Settings(BaseSettings):
|
||||||
default=False, validation_alias="AVITO_DETAIL_BACKFILL_USE_CURL"
|
default=False, validation_alias="AVITO_DETAIL_BACKFILL_USE_CURL"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# reuse_context для BrowserFetcher в avito_detail_backfill (browser-режим).
|
||||||
|
# ENV: AVITO_DETAIL_REUSE_CONTEXT.
|
||||||
|
#
|
||||||
|
# Дефолт False (замер 19.09.2026, probe-browser, прокси узел 14, схема A-B-A,
|
||||||
|
# 16 карточек ЕКБ на плечо): A1 (новый изолированный context на карточку) 14/16,
|
||||||
|
# B (reuse_context=True + origin/referer) 6/16, A2 (снова изолированный) 13/16.
|
||||||
|
# В плече B отказы нарастали по ходу — карточки 1-3 успешны, последние пять
|
||||||
|
# подряд бан: выгорание контекста, а не совпадение. Живые куки pow_challenge/
|
||||||
|
# pow_solved у Авито держатся ~5 минут (замер: выданы 23:08, истекли 23:13),
|
||||||
|
# а прогон обогащения идёт десятки минут с паузой 5-6с между карточками — в
|
||||||
|
# переиспользуемом context'е токен протухает и не обновляется, дальше каждая
|
||||||
|
# карточка получает бан-страницу. В свежем context'е навигация решает PoW
|
||||||
|
# заново. Решение про reuse_context=True перенесено на Авито по аналогии с
|
||||||
|
# DomClick (#3118, QRATOR-PoW, 26/26 блоков на изолированных контекстах против
|
||||||
|
# 5/5 успехов в тёплом) — для DomClick это по-прежнему верно, для Авито
|
||||||
|
# опровергнуто измерением. Флаг — аварийный откат без выката кода.
|
||||||
|
avito_detail_reuse_context: bool = Field(
|
||||||
|
default=False, validation_alias="AVITO_DETAIL_REUSE_CONTEXT"
|
||||||
|
)
|
||||||
|
|
||||||
# #1950: hard-timeout на один detail-fetch внутри avito_detail_backfill. Зависший
|
# #1950: hard-timeout на один detail-fetch внутри avito_detail_backfill. Зависший
|
||||||
# fetch_detail (camoufox/browser hang или curl-stall) блокирует loop навсегда →
|
# fetch_detail (camoufox/browser hang или curl-stall) блокирует loop навсегда →
|
||||||
# budget-guard (раз в итерацию) не срабатывает → heartbeat не обновляется → run
|
# budget-guard (раз в итерацию) не срабатывает → heartbeat не обновляется → run
|
||||||
|
|
|
||||||
|
|
@ -411,13 +411,22 @@ async def run_avito_detail_backfill(
|
||||||
# Замер 27.08: прогон 5098 — mode=browser, proxy_lease_id=None,
|
# Замер 27.08: прогон 5098 — mode=browser, proxy_lease_id=None,
|
||||||
# 5 блоков подряд из 5 попыток, enriched=0. При этом пул здоров
|
# 5 блоков подряд из 5 попыток, enriched=0. При этом пул здоров
|
||||||
# (4 узла, все ok), а тот же URL через прокси отдаёт 200 и 3.3 МБ.
|
# (4 узла, все ok), а тот же URL через прокси отдаёт 200 и 3.3 МБ.
|
||||||
# reuse_context=True (#3180/#3251): без него sidecar's browser.new_page()
|
#
|
||||||
# создаёт НОВЫЙ изолированный context на КАЖДЫЙ /fetch — пройденный
|
# reuse_context управляется settings.avito_detail_reuse_context, дефолт
|
||||||
# QRATOR-PoW предыдущей карточки выбрасывается, и следующий запрос снова
|
# False (#3180/#3251 -> опровергнуто новым замером, см. config.py).
|
||||||
# холодный. У DomClick (#3118) это измеренно давало 100% блоков (26/26)
|
# Изначально сюда перенесли reuse_context=True по аналогии с DomClick
|
||||||
# на изолированных контекстах против 5/5 успехов в тёплом контексте.
|
# (#3118, QRATOR-PoW): на изолированных контекстах 26/26 блоков против
|
||||||
# Сброс сожжённого context'а — ниже, через bf.request_context_reset()
|
# 5/5 успехов в тёплом — для DomClick это по-прежнему верно и не
|
||||||
# (#3212: один раз за прогон, не на каждый блок — см. except-ветку).
|
# менялось. Для Авито замер 19.09.2026 (probe-browser, прокси узел 14,
|
||||||
|
# схема A-B-A, 16 карточек ЕКБ на плечо) дал обратный результат:
|
||||||
|
# изолированный context 14/16 и 13/16, тёплый reuse_context=True 6/16
|
||||||
|
# с нарастающими отказами к концу плеча. Причина — куки Авито
|
||||||
|
# pow_challenge/pow_solved живут ~5 минут, а прогон между карточками
|
||||||
|
# растянут на десятки минут: в тёплом context'е токен протухает и не
|
||||||
|
# обновляется, свежая навигация решает PoW заново. Сброс сожжённого
|
||||||
|
# context'а (актуален только при reuse_context=True) — ниже, через
|
||||||
|
# bf.request_context_reset() (#3212: один раз за прогон, не на каждый
|
||||||
|
# блок — см. except-ветку).
|
||||||
_cfg = RealScraperConfig()
|
_cfg = RealScraperConfig()
|
||||||
browser_fetcher = BrowserFetcher(
|
browser_fetcher = BrowserFetcher(
|
||||||
source="avito",
|
source="avito",
|
||||||
|
|
@ -427,7 +436,7 @@ async def run_avito_detail_backfill(
|
||||||
# Без environment отказ «пул пуст» на этом пути мёртв — фетчер
|
# Без environment отказ «пул пуст» на этом пути мёртв — фетчер
|
||||||
# молча ушёл бы на env-прокси сайдкара (#2616 шаг 1).
|
# молча ушёл бы на env-прокси сайдкара (#2616 шаг 1).
|
||||||
environment=_cfg.environment,
|
environment=_cfg.environment,
|
||||||
reuse_context=True,
|
reuse_context=settings.avito_detail_reuse_context,
|
||||||
)
|
)
|
||||||
await browser_fetcher.__aenter__()
|
await browser_fetcher.__aenter__()
|
||||||
own_browser = True
|
own_browser = True
|
||||||
|
|
@ -576,12 +585,15 @@ async def run_avito_detail_backfill(
|
||||||
# прокси. Обнуляется на каждой ротации (успешной ИЛИ неуспешной — иначе
|
# прокси. Обнуляется на каждой ротации (успешной ИЛИ неуспешной — иначе
|
||||||
# исчерпанный дневной лимит провайдера дёргал бы rotate_proxy на каждой попытке).
|
# исчерпанный дневной лимит провайдера дёргал бы rotate_proxy на каждой попытке).
|
||||||
attempts_since_rotation = 0
|
attempts_since_rotation = 0
|
||||||
# #3251: сброс переиспользуемого browser-context'а (reuse_context=True выше)
|
# #3251: сброс переиспользуемого browser-context'а разрешён РОВНО один раз
|
||||||
# разрешён РОВНО один раз за прогон — зеркалит domclick_detail_backfill (#3212).
|
# за прогон — зеркалит domclick_detail_backfill (#3212). Сброс на КАЖДЫЙ
|
||||||
# Сброс на КАЖДЫЙ блок сам себя поддерживает: пройденный QRATOR-PoW живёт в
|
# блок сам себя поддерживает: пройденный QRATOR-PoW живёт в context'е,
|
||||||
# context'е, сброс его выбрасывает, повторная проверка с того же IP сразу
|
# сброс его выбрасывает, повторная проверка с того же IP сразу после
|
||||||
# после принятой снова блокируется — одна осечка превращается в необратимый
|
# принятой снова блокируется — одна осечка превращается в необратимый
|
||||||
# каскад блоков (см. except-ветку ниже).
|
# каскад блоков (см. except-ветку ниже). Осмыслен только при
|
||||||
|
# settings.avito_detail_reuse_context=True — при выключенном
|
||||||
|
# переиспользовании (дефолт) bare-reset ниже не вызывается вовсе: каждый
|
||||||
|
# fetch и так идёт в свежем изолированном context'е.
|
||||||
context_reset_used = False
|
context_reset_used = False
|
||||||
# #3283g: сколько раз ЗА ПРОГОН уже сработала ротация-на-бан (в отличие от
|
# #3283g: сколько раз ЗА ПРОГОН уже сработала ротация-на-бан (в отличие от
|
||||||
# context_reset_used эта ротация меняет IP вместе со сбросом, поэтому не
|
# context_reset_used эта ротация меняет IP вместе со сбросом, поэтому не
|
||||||
|
|
@ -901,7 +913,9 @@ async def run_avito_detail_backfill(
|
||||||
):
|
):
|
||||||
context_reset_used = True
|
context_reset_used = True
|
||||||
|
|
||||||
if not context_reset_used:
|
# Бессмысленно при reuse_context=False (дефолт): каждый fetch и
|
||||||
|
# так идёт в свежем изолированном context'е, сбрасывать нечего.
|
||||||
|
if settings.avito_detail_reuse_context and not context_reset_used:
|
||||||
context_reset_used = True
|
context_reset_used = True
|
||||||
browser_fetcher.request_context_reset()
|
browser_fetcher.request_context_reset()
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
"""Тёплый переиспользуемый browser-context для Avito detail-бэкфилла (#3180/#3251).
|
"""Переиспользуемый browser-context для Avito detail-бэкфилла (#3180/#3251/#3595).
|
||||||
|
|
||||||
Переносит на Avito рецепт, измеренно поднявший добор карточек у DomClick (#3118):
|
Рецепт reuse_context=True изначально перенесли на Avito по аналогии с DomClick
|
||||||
sidecar's browser.new_page() создаёт НОВЫЙ изолированный context на КАЖДЫЙ /fetch —
|
(#3118): sidecar's browser.new_page() создаёт НОВЫЙ изолированный context на
|
||||||
пройденный QRATOR-подобный анти-бот проход площадки выбрасывается вместе с ним, и
|
КАЖДЫЙ /fetch — пройденный анти-бот проход площадки выбрасывается вместе с ним, и
|
||||||
каждая следующая карточка снова холодная. reuse_context=True держит ОДИН sidecar-
|
каждая следующая карточка снова холодная. Для DomClick это подтверждено (26/26
|
||||||
context живым на весь прогон; сброс сожжённого context'а оркестратор просит через
|
блоков на изолированных контекстах против 5/5 успехов в тёплом). Для Avito замер
|
||||||
bf.request_context_reset() — как и DomClick, ровно один раз за прогон (#3212), а не
|
19.09.2026 (probe-browser, A-B-A, 16 карточек ЕКБ/плечо) дал обратный результат:
|
||||||
на каждый обнаруженный блок (иначе сброс сам себя поддерживает — см. domclick
|
изолированный context 14/16 и 13/16, reuse_context=True 6/16 с нарастающими
|
||||||
docstring). Reset относится ТОЛЬКО к реальному бану площадкой (AvitoBlockedError и
|
отказами к концу плеча — 5-минутная PoW-кука протухает внутри растянутого на
|
||||||
подтипы), не к отказу нашего тракта (AvitoSidecarUnavailableError) и не к мёртвому
|
десятки минут прогона и не обновляется в тёплом context'е. Дефолт теперь
|
||||||
листингу (AvitoListingGoneError) — площадка тут ни при чём в обоих случаях, context
|
settings.avito_detail_reuse_context=False; флаг остаётся аварийным откатом.
|
||||||
цел.
|
|
||||||
|
Когда reuse_context=True (явный override), сброс сожжённого context'а
|
||||||
|
оркестратор просит через bf.request_context_reset() — как и DomClick, ровно один
|
||||||
|
раз за прогон (#3212), а не на каждый обнаруженный блок (иначе сброс сам себя
|
||||||
|
поддерживает — см. domclick docstring). Reset относится ТОЛЬКО к реальному бану
|
||||||
|
площадкой (AvitoBlockedError и подтипы), не к отказу нашего тракта
|
||||||
|
(AvitoSidecarUnavailableError) и не к мёртвому листингу (AvitoListingGoneError) —
|
||||||
|
площадка тут ни при чём в обоих случаях, context цел. При reuse_context=False
|
||||||
|
(дефолт) reset не вызывается вовсе — сбрасывать нечего.
|
||||||
|
|
||||||
Контракт тестов сформулирован от требования площадки/архитектуры (что ДОЛЖНО
|
Контракт тестов сформулирован от требования площадки/архитектуры (что ДОЛЖНО
|
||||||
происходить при блоке/не-блоке), а не от текущей реализации — см. #3262, где
|
происходить при блоке/не-блоке), а не от текущей реализации — см. #3262, где
|
||||||
|
|
@ -80,6 +88,9 @@ def _fake_settings(**overrides: object) -> MagicMock:
|
||||||
# rotate-on-ban веткой -- она молчаливо остаётся в "gap не выдержан".
|
# rotate-on-ban веткой -- она молчаливо остаётся в "gap не выдержан".
|
||||||
"avito_detail_backfill_rotate_on_ban_max": 2,
|
"avito_detail_backfill_rotate_on_ban_max": 2,
|
||||||
"avito_detail_backfill_rotate_on_ban_min_gap": 10,
|
"avito_detail_backfill_rotate_on_ban_min_gap": 10,
|
||||||
|
# Реальный дефолт (config.py, замер 19.09.2026) — изолированный context на
|
||||||
|
# каждый fetch. Тесты, которым нужен тёплый context, переопределяют явно.
|
||||||
|
"avito_detail_reuse_context": False,
|
||||||
}
|
}
|
||||||
defaults.update(overrides)
|
defaults.update(overrides)
|
||||||
return MagicMock(**defaults)
|
return MagicMock(**defaults)
|
||||||
|
|
@ -118,12 +129,17 @@ def _mock_browser_fetcher_cls() -> tuple[MagicMock, AsyncMock]:
|
||||||
return MagicMock(return_value=instance), instance
|
return MagicMock(return_value=instance), instance
|
||||||
|
|
||||||
|
|
||||||
# ── BrowserFetcher(reuse_context=True) ──────────────────────────────────────────
|
# ── BrowserFetcher(reuse_context=<settings.avito_detail_reuse_context>) ─────────
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_backfill_constructs_browser_fetcher_with_reuse_context() -> None:
|
async def test_backfill_constructs_browser_fetcher_with_reuse_context_default_false() -> None:
|
||||||
"""BrowserFetcher поднимается с reuse_context=True (#3180) -- тёплый sidecar-context."""
|
"""Дефолт settings.avito_detail_reuse_context=False -- fetcher идёт с ним же.
|
||||||
|
|
||||||
|
Замер 19.09.2026 (probe-browser, A-B-A, 16 карточек ЕКБ/плечо): изолированный
|
||||||
|
context 14/16 и 13/16, reuse_context=True 6/16 с нарастающими отказами --
|
||||||
|
выгорание ~5-минутной PoW-куки. См. config.py docstring.
|
||||||
|
"""
|
||||||
snapshot = _make_snapshot(1)
|
snapshot = _make_snapshot(1)
|
||||||
db = _mock_db(snapshot)
|
db = _mock_db(snapshot)
|
||||||
runs = MagicMock()
|
runs = MagicMock()
|
||||||
|
|
@ -147,6 +163,37 @@ async def test_backfill_constructs_browser_fetcher_with_reuse_context() -> None:
|
||||||
mock_bf_cls.assert_called_once()
|
mock_bf_cls.assert_called_once()
|
||||||
_, call_kwargs = mock_bf_cls.call_args
|
_, call_kwargs = mock_bf_cls.call_args
|
||||||
assert call_kwargs["source"] == "avito"
|
assert call_kwargs["source"] == "avito"
|
||||||
|
assert call_kwargs["reuse_context"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_backfill_forwards_reuse_context_from_settings_not_constant() -> None:
|
||||||
|
"""reuse_context идёт из settings.avito_detail_reuse_context, не из хардкода.
|
||||||
|
|
||||||
|
Аварийный откат на тёплый context (settings=True) должен доходить до
|
||||||
|
BrowserFetcher без правки кода.
|
||||||
|
"""
|
||||||
|
snapshot = _make_snapshot(1)
|
||||||
|
db = _mock_db(snapshot)
|
||||||
|
runs = MagicMock()
|
||||||
|
mock_fetch = AsyncMock(return_value=MagicMock())
|
||||||
|
fake_settings = _fake_settings(avito_detail_reuse_context=True)
|
||||||
|
mock_bf_cls, _instance = _mock_browser_fetcher_cls()
|
||||||
|
with (
|
||||||
|
patch(_SETTINGS, fake_settings),
|
||||||
|
patch(_SESSION),
|
||||||
|
patch(_SCRAPER),
|
||||||
|
patch(_RUNS, runs),
|
||||||
|
patch(_BROWSER_FETCHER, mock_bf_cls),
|
||||||
|
patch(_FETCH, mock_fetch),
|
||||||
|
patch(_SAVE, return_value=True),
|
||||||
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
|
):
|
||||||
|
await run_avito_detail_backfill(
|
||||||
|
db, run_id=209, params={"batch_size": 10, "budget_sec": 3600}
|
||||||
|
)
|
||||||
|
|
||||||
|
_, call_kwargs = mock_bf_cls.call_args
|
||||||
assert call_kwargs["reuse_context"] is True
|
assert call_kwargs["reuse_context"] is True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -155,18 +202,20 @@ async def test_backfill_constructs_browser_fetcher_with_reuse_context() -> None:
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_context_reset_requested_at_most_once_per_run() -> None:
|
async def test_context_reset_requested_at_most_once_per_run() -> None:
|
||||||
"""3 подряд AvitoBlockedError -> request_context_reset() запрошен ровно 1 раз.
|
"""3 подряд AvitoBlockedError + reuse_context=True -> reset запрошен ровно 1 раз.
|
||||||
|
|
||||||
Зеркалит domclick #3212: сброс на КАЖДЫЙ блок сам себя поддерживает (пройденный
|
Зеркалит domclick #3212: сброс на КАЖДЫЙ блок сам себя поддерживает (пройденный
|
||||||
анти-бот проход живёт в context'е, сброс его выбрасывает -> следующая проверка с
|
анти-бот проход живёт в context'е, сброс его выбрасывает -> следующая проверка с
|
||||||
того же IP снова блокируется) -- одна осечка становится необратимым каскадом.
|
того же IP снова блокируется) -- одна осечка становится необратимым каскадом.
|
||||||
|
Актуально только при reuse_context=True -- при дефолтном False см.
|
||||||
|
test_context_reset_not_requested_when_reuse_context_disabled ниже.
|
||||||
"""
|
"""
|
||||||
snapshot = _make_snapshot(10)
|
snapshot = _make_snapshot(10)
|
||||||
db = _mock_db(snapshot)
|
db = _mock_db(snapshot)
|
||||||
runs = MagicMock()
|
runs = MagicMock()
|
||||||
blocked_exc = AvitoBlockedError("firewall/soft-block")
|
blocked_exc = AvitoBlockedError("firewall/soft-block")
|
||||||
mock_fetch = AsyncMock(side_effect=blocked_exc)
|
mock_fetch = AsyncMock(side_effect=blocked_exc)
|
||||||
fake_settings = _fake_settings()
|
fake_settings = _fake_settings(avito_detail_reuse_context=True)
|
||||||
mock_bf_cls, instance = _mock_browser_fetcher_cls()
|
mock_bf_cls, instance = _mock_browser_fetcher_cls()
|
||||||
with (
|
with (
|
||||||
patch(_SETTINGS, fake_settings),
|
patch(_SETTINGS, fake_settings),
|
||||||
|
|
@ -196,7 +245,7 @@ async def test_content_blocked_subtype_also_requests_reset() -> None:
|
||||||
db = _mock_db(snapshot)
|
db = _mock_db(snapshot)
|
||||||
runs = MagicMock()
|
runs = MagicMock()
|
||||||
mock_fetch = AsyncMock(side_effect=AvitoContentBlockedError("0 карточек, captcha"))
|
mock_fetch = AsyncMock(side_effect=AvitoContentBlockedError("0 карточек, captcha"))
|
||||||
fake_settings = _fake_settings()
|
fake_settings = _fake_settings(avito_detail_reuse_context=True)
|
||||||
mock_bf_cls, instance = _mock_browser_fetcher_cls()
|
mock_bf_cls, instance = _mock_browser_fetcher_cls()
|
||||||
with (
|
with (
|
||||||
patch(_SETTINGS, fake_settings),
|
patch(_SETTINGS, fake_settings),
|
||||||
|
|
@ -217,6 +266,40 @@ async def test_content_blocked_subtype_also_requests_reset() -> None:
|
||||||
instance.request_context_reset.assert_called_once()
|
instance.request_context_reset.assert_called_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_context_reset_not_requested_when_reuse_context_disabled() -> None:
|
||||||
|
"""reuse_context=False (дефолт, замер 19.09.2026) -- bare reset не вызывается вовсе.
|
||||||
|
|
||||||
|
Каждый fetch уже идёт в свежем изолированном context'е -- сбрасывать нечего,
|
||||||
|
даже когда блоки идут подряд. Отличает дефолт от test_context_reset_requested_
|
||||||
|
at_most_once_per_run, где то же самое AvitoBlockedError с reuse_context=True
|
||||||
|
даёт ровно один reset.
|
||||||
|
"""
|
||||||
|
snapshot = _make_snapshot(10)
|
||||||
|
db = _mock_db(snapshot)
|
||||||
|
runs = MagicMock()
|
||||||
|
mock_fetch = AsyncMock(side_effect=AvitoBlockedError("firewall/soft-block"))
|
||||||
|
fake_settings = _fake_settings()
|
||||||
|
mock_bf_cls, instance = _mock_browser_fetcher_cls()
|
||||||
|
with (
|
||||||
|
patch(_SETTINGS, fake_settings),
|
||||||
|
patch(_SESSION),
|
||||||
|
patch(_SCRAPER),
|
||||||
|
patch(_RUNS, runs),
|
||||||
|
patch(_BROWSER_FETCHER, mock_bf_cls),
|
||||||
|
patch(_FETCH, mock_fetch),
|
||||||
|
patch(_SLEEP, new_callable=AsyncMock),
|
||||||
|
):
|
||||||
|
result = await run_avito_detail_backfill(
|
||||||
|
db,
|
||||||
|
run_id=210,
|
||||||
|
params={"batch_size": 10, "budget_sec": 3600, "max_consecutive_blocks": 3},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.blocked == 3
|
||||||
|
instance.request_context_reset.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_sidecar_unavailable_does_not_request_context_reset() -> None:
|
async def test_sidecar_unavailable_does_not_request_context_reset() -> None:
|
||||||
"""AvitoSidecarUnavailableError -- НАШ тракт, площадка ни при чём: reset НЕ звать.
|
"""AvitoSidecarUnavailableError -- НАШ тракт, площадка ни при чём: reset НЕ звать.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""avito_detail_reuse_context — дефолт False (замер 19.09.2026, probe-browser).
|
||||||
|
|
||||||
|
Схема A-B-A, 16 карточек ЕКБ на плечо, один прокси-узел: изолированный context
|
||||||
|
14/16 и 13/16, reuse_context=True (тёплый) 6/16 с нарастающими отказами к концу
|
||||||
|
плеча — выгорание PoW-куки (~5 минут жизни) внутри растянутого на десятки минут
|
||||||
|
прогона. Перенесено на Avito по аналогии с DomClick (#3118) и опровергнуто
|
||||||
|
измерением; для DomClick reuse_context остаётся верным решением. Проверка по
|
||||||
|
значению поля Settings без env и без .env-файла — зеркалит test_3045_use_curl_default.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/test")
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.core.config import Settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_avito_detail_reuse_context_defaults_to_false(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delenv("AVITO_DETAIL_REUSE_CONTEXT", raising=False)
|
||||||
|
|
||||||
|
assert Settings(_env_file=None).avito_detail_reuse_context is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_still_switches_reuse_context_on(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Контроль: переменная окружения по-прежнему включает режим — поле живое.
|
||||||
|
|
||||||
|
Флаг остаётся аварийным откатом на тёплый context без выката кода.
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("AVITO_DETAIL_REUSE_CONTEXT", "true")
|
||||||
|
|
||||||
|
assert Settings(_env_file=None).avito_detail_reuse_context is True
|
||||||
Loading…
Add table
Reference in a new issue