fix(tradein/browser): повтор evaluate на той же странице — гонка случается ПОСЛЕ load (#2676) #2721
2 changed files with 188 additions and 116 deletions
|
|
@ -1005,30 +1005,54 @@ async def _do_fetch_json(
|
||||||
return await _fetch_json_once(
|
return await _fetch_json_once(
|
||||||
provider, url, method=method, headers=headers, body=body, origin=origin
|
provider, url, method=method, headers=headers, body=body, origin=origin
|
||||||
)
|
)
|
||||||
if _is_page_context_lost(exc):
|
# #2676 обрабатывается ВНУТРИ _fetch_json_once (повтор evaluate на той же
|
||||||
# #2676: браузер жив, умерла ОДНА страница — relaunch не нужен (стоил бы
|
# странице). Повторять здесь, на свежей странице, бесполезно: пересоздание
|
||||||
# ~10-20с и тёплые cookies инстанса). Повторяем на свежей странице, но с
|
# воспроизводит ту же первую навигацию origin'а — проверено на проде 06.08,
|
||||||
# ожиданием `load`: без него повтор попадает в то же окно клиентской
|
# обе попытки упали одинаково.
|
||||||
# навигации, и «транзиентная» ошибка воспроизводится детерминированно.
|
|
||||||
logger.warning(
|
|
||||||
"tradein-browser[%s]: страница ушла в навигацию (%s), retry fetch-json "
|
|
||||||
"с ожиданием load: %s",
|
|
||||||
provider,
|
|
||||||
type(exc).__name__,
|
|
||||||
url,
|
|
||||||
)
|
|
||||||
return await _fetch_json_once(
|
|
||||||
provider,
|
|
||||||
url,
|
|
||||||
method=method,
|
|
||||||
headers=headers,
|
|
||||||
body=body,
|
|
||||||
origin=origin,
|
|
||||||
wait_for_load=True,
|
|
||||||
)
|
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# In-page retry самого fetch() при СЕТЕВОМ throw (TypeError/"NetworkError"), #1917:
|
||||||
|
# первый XHR после навигации иногда падает до готовности стека. Повтор внутри страницы
|
||||||
|
# (~retryDelayMs) дешевле полной ре-навигации (~30-45с/дом). HTTP-статусы (4xx/5xx) —
|
||||||
|
# это успешный resp, НЕ ретраим (решает caller).
|
||||||
|
_IN_PAGE_FETCH_JS = """async ({url, method, headers, body, retries, retryDelayMs}) => {
|
||||||
|
let lastErr;
|
||||||
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(url, {
|
||||||
|
method: method || 'GET',
|
||||||
|
headers: headers || {},
|
||||||
|
body: (body !== null && body !== undefined) ? body : undefined,
|
||||||
|
credentials: 'include',
|
||||||
|
});
|
||||||
|
const text = await resp.text();
|
||||||
|
return { status: resp.status, body: text, attempts: attempt + 1 };
|
||||||
|
} catch (e) {
|
||||||
|
lastErr = e;
|
||||||
|
if (attempt < retries) {
|
||||||
|
await new Promise(r => setTimeout(r, retryDelayMs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw lastErr;
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
|
async def _wait_for_load_best_effort(page: object, provider: str) -> None:
|
||||||
|
"""Ждёт `load`, но не даёт ожиданию сорвать саму попытку (реклама/трекеры могут
|
||||||
|
держать страницу «загружающейся» бесконечно). Таймаут логируется, не пробрасывается.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await page.wait_for_load_state("load", timeout=FETCH_JSON_LOAD_WAIT_MS) # type: ignore[attr-defined]
|
||||||
|
except Exception as exc:
|
||||||
|
logger.info(
|
||||||
|
"tradein-browser[%s]: load не дождались (%s), пробуем evaluate как есть",
|
||||||
|
provider,
|
||||||
|
type(exc).__name__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _fetch_json_once(
|
async def _fetch_json_once(
|
||||||
provider: str,
|
provider: str,
|
||||||
url: str,
|
url: str,
|
||||||
|
|
@ -1037,7 +1061,6 @@ async def _fetch_json_once(
|
||||||
headers: dict,
|
headers: dict,
|
||||||
body: object,
|
body: object,
|
||||||
origin: str,
|
origin: str,
|
||||||
wait_for_load: bool = False,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Переходит на origin (same-origin якорь) и выполняет in-page fetch(url).
|
"""Переходит на origin (same-origin якорь) и выполняет in-page fetch(url).
|
||||||
|
|
||||||
|
|
@ -1056,62 +1079,39 @@ async def _fetch_json_once(
|
||||||
await _apply_resource_block(page)
|
await _apply_resource_block(page)
|
||||||
await _pace_provider(provider)
|
await _pace_provider(provider)
|
||||||
await page.goto(origin, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded") # type: ignore[attr-defined]
|
await page.goto(origin, timeout=BROWSER_NAV_TIMEOUT_MS, wait_until="domcontentloaded") # type: ignore[attr-defined]
|
||||||
if wait_for_load:
|
|
||||||
# Только ретрай после #2676: даём клиентской навигации доиграть до `load`,
|
|
||||||
# иначе повтор попадает ровно в то же окно и падает так же.
|
|
||||||
try:
|
|
||||||
await page.wait_for_load_state( # type: ignore[attr-defined]
|
|
||||||
"load", timeout=FETCH_JSON_LOAD_WAIT_MS
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
|
||||||
# Best-effort: страница может дозагружаться бесконечно (реклама/трекеры).
|
|
||||||
# Не пробрасываем — evaluate ниже сам скажет, готова страница или нет;
|
|
||||||
# молчать нельзя, поэтому пишем в лог.
|
|
||||||
logger.info(
|
|
||||||
"tradein-browser[%s]: load не дождались (%s), пробуем evaluate как есть",
|
|
||||||
provider,
|
|
||||||
type(exc).__name__,
|
|
||||||
)
|
|
||||||
# БЕЗ полного BROWSER_WAIT_MS: нам нужен лишь origin-контекст (cookies +
|
# БЕЗ полного BROWSER_WAIT_MS: нам нужен лишь origin-контекст (cookies +
|
||||||
# same-origin scope для fetch), а не отрендеренные listings. Settle-паузы
|
# same-origin scope для fetch), а не отрендеренные listings. Settle-паузы
|
||||||
# (#1917, FETCH_JSON_SETTLE_MS) хватает, чтобы страница инициализировалась
|
# (#1917, FETCH_JSON_SETTLE_MS) хватает, чтобы страница инициализировалась
|
||||||
# перед in-page fetch'ем.
|
# перед in-page fetch'ем.
|
||||||
await page.wait_for_timeout(FETCH_JSON_SETTLE_MS) # type: ignore[attr-defined]
|
await page.wait_for_timeout(FETCH_JSON_SETTLE_MS) # type: ignore[attr-defined]
|
||||||
# In-page retry самого fetch() при СЕТЕВОМ throw (TypeError/"NetworkError"),
|
payload = {
|
||||||
# #1917: первый XHR после навигации иногда падает до готовности стека. Повтор
|
"url": url,
|
||||||
# внутри страницы (~retryDelayMs) дешевле полной ре-навигации (~30-45с/дом).
|
"method": method,
|
||||||
# HTTP-статусы (4xx/5xx) — это успешный resp, НЕ ретраим (решает caller).
|
"headers": headers or {},
|
||||||
result: dict = await page.evaluate( # type: ignore[attr-defined]
|
"body": body,
|
||||||
"""async ({url, method, headers, body, retries, retryDelayMs}) => {
|
"retries": FETCH_JSON_INPAGE_RETRIES,
|
||||||
let lastErr;
|
"retryDelayMs": FETCH_JSON_RETRY_DELAY_MS,
|
||||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
}
|
||||||
try {
|
try:
|
||||||
const resp = await fetch(url, {
|
result: dict = await page.evaluate(_IN_PAGE_FETCH_JS, payload) # type: ignore[attr-defined]
|
||||||
method: method || 'GET',
|
except Exception as exc:
|
||||||
headers: headers || {},
|
if not _is_page_context_lost(exc):
|
||||||
body: (body !== null && body !== undefined) ? body : undefined,
|
raise
|
||||||
credentials: 'include',
|
# #2676, живой прод 06.08: страница origin уходит в клиентскую навигацию
|
||||||
});
|
# ПОСЛЕ load, поэтому ни settle, ни ожидание load её не опережают, а повтор
|
||||||
const text = await resp.text();
|
# с новой страницей воспроизводит ту же первую навигацию (проверено на
|
||||||
return { status: resp.status, body: text, attempts: attempt + 1 };
|
# проде: две попытки подряд — та же ошибка). Навигация при этом ОДНА:
|
||||||
} catch (e) {
|
# к моменту исключения она уже случилась и у страницы НОВЫЙ контекст.
|
||||||
lastErr = e;
|
# Поэтому повторяем evaluate на ЭТОЙ же странице — стоит миллисекунды.
|
||||||
if (attempt < retries) {
|
logger.warning(
|
||||||
await new Promise(r => setTimeout(r, retryDelayMs));
|
"tradein-browser[%s]: контекст страницы пересоздан навигацией, "
|
||||||
}
|
"повтор evaluate на той же странице: %s",
|
||||||
}
|
provider,
|
||||||
}
|
url,
|
||||||
throw lastErr;
|
)
|
||||||
}""",
|
await _wait_for_load_best_effort(page, provider)
|
||||||
{
|
await page.wait_for_timeout(FETCH_JSON_SETTLE_MS) # type: ignore[attr-defined]
|
||||||
"url": url,
|
result = await page.evaluate(_IN_PAGE_FETCH_JS, payload) # type: ignore[attr-defined]
|
||||||
"method": method,
|
|
||||||
"headers": headers or {},
|
|
||||||
"body": body,
|
|
||||||
"retries": FETCH_JSON_INPAGE_RETRIES,
|
|
||||||
"retryDelayMs": FETCH_JSON_RETRY_DELAY_MS,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
finally:
|
finally:
|
||||||
await page.close() # type: ignore[attr-defined]
|
await page.close() # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -374,17 +374,88 @@ def test_do_fetch_json_relaunch_on_browser_crash(monkeypatch: pytest.MonkeyPatch
|
||||||
# ── гонка «execution context was destroyed» (#2676) ───────────────────────────────
|
# ── гонка «execution context was destroyed» (#2676) ───────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
class _FakeSequenceBrowser:
|
def test_fetch_json_retries_evaluate_on_the_same_page(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Отдаёт страницы по очереди: первая попытка ≠ вторая (retry на СВЕЖЕЙ странице)."""
|
"""#2676 (живой прод 06.08): навигация УЖЕ случилась → повтор evaluate на той же странице.
|
||||||
|
|
||||||
def __init__(self, pages: list[_FakePage]) -> None:
|
Ключевое отличие от внешнего ретрая: страница НЕ пересоздаётся. Пересоздание
|
||||||
self._pages = list(pages)
|
воспроизводит ту же первую навигацию origin'а (проверено на проде: обе попытки
|
||||||
self.opened = 0
|
падали одинаково), а после исключения у страницы уже новый, стабильный контекст.
|
||||||
|
"""
|
||||||
|
page = _FakePage({"status": 200, "body": "ok"})
|
||||||
|
page.evaluate = AsyncMock(
|
||||||
|
side_effect=[
|
||||||
|
RuntimeError("Page.evaluate: Execution context was destroyed, "
|
||||||
|
"most likely because of a navigation."),
|
||||||
|
{"status": 200, "body": '{"recovered": true}'},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
browser = _FakeBrowser(page)
|
||||||
|
server._browsers["avito"] = browser
|
||||||
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
|
monkeypatch.setattr(server, "FETCH_JSON_LOAD_WAIT_MS", 4242)
|
||||||
|
|
||||||
async def new_page(self) -> _FakePage:
|
result = asyncio.run(
|
||||||
page = self._pages[min(self.opened, len(self._pages) - 1)]
|
server._fetch_json_once(
|
||||||
self.opened += 1
|
"avito",
|
||||||
return page
|
"https://www.avito.ru/api/x",
|
||||||
|
method="GET",
|
||||||
|
headers={},
|
||||||
|
body=None,
|
||||||
|
origin="https://www.avito.ru/",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == {"status": 200, "body": '{"recovered": true}'}
|
||||||
|
assert page.evaluate.await_count == 2
|
||||||
|
# Ни новой страницы, ни повторной навигации — только ожидание конца текущей.
|
||||||
|
assert browser.opened == 1
|
||||||
|
assert page.goto_urls == ["https://www.avito.ru/"]
|
||||||
|
assert page.load_waits == [4242]
|
||||||
|
assert page.closed == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_json_same_page_retry_happens_once(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Повтор evaluate ровно один: вторая та же ошибка уезжает наверх (там внешняя ветка)."""
|
||||||
|
message = "Execution context was destroyed"
|
||||||
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
|
page.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
||||||
|
server._browsers["avito"] = _FakeBrowser(page)
|
||||||
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="Execution context was destroyed"):
|
||||||
|
asyncio.run(
|
||||||
|
server._fetch_json_once(
|
||||||
|
"avito",
|
||||||
|
"https://www.avito.ru/api/x",
|
||||||
|
method="GET",
|
||||||
|
headers={},
|
||||||
|
body=None,
|
||||||
|
origin="https://www.avito.ru/",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
assert page.evaluate.await_count == 2
|
||||||
|
assert page.closed == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_json_no_same_page_retry_for_unrelated_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
"""Чужая ошибка evaluate не даёт второго прохода — повтор стоит денег."""
|
||||||
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
|
page.evaluate = AsyncMock(side_effect=RuntimeError("boom"))
|
||||||
|
server._browsers["avito"] = _FakeBrowser(page)
|
||||||
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError, match="boom"):
|
||||||
|
asyncio.run(
|
||||||
|
server._fetch_json_once(
|
||||||
|
"avito",
|
||||||
|
"https://www.avito.ru/api/x",
|
||||||
|
method="GET",
|
||||||
|
headers={},
|
||||||
|
body=None,
|
||||||
|
origin="https://www.avito.ru/",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
page.evaluate.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
def _no_relaunch(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
def _no_relaunch(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
||||||
|
|
@ -406,21 +477,21 @@ def _no_relaunch(monkeypatch: pytest.MonkeyPatch) -> list[str]:
|
||||||
"Execution context was destroyed",
|
"Execution context was destroyed",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_do_fetch_json_retries_on_destroyed_context(
|
def test_do_fetch_json_recovers_from_destroyed_context(
|
||||||
monkeypatch: pytest.MonkeyPatch, message: str
|
monkeypatch: pytest.MonkeyPatch, message: str
|
||||||
) -> None:
|
) -> None:
|
||||||
"""#2676: страница ушла в навигацию → повтор на свежей странице, БЕЗ relaunch.
|
"""#2676: обе формы сообщения playwright распознаются и переживаются повтором.
|
||||||
|
|
||||||
До правки такая ошибка не попадала ни в одну ветку восстановления (_is_browser_crash
|
До правки такая ошибка не попадала ни в одну ветку восстановления (_is_browser_crash
|
||||||
матчит только закрытие цели/браузера/соединения) и уезжала наверх как 500.
|
матчит только закрытие цели/браузера/соединения) и уезжала наверх как 500.
|
||||||
"""
|
"""
|
||||||
racing_page = _FakePage({"status": 0, "body": ""})
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
racing_page.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
page.evaluate = AsyncMock(
|
||||||
settled_page = _FakePage({"status": 200, "body": '{"recovered": true}'})
|
side_effect=[RuntimeError(message), {"status": 200, "body": '{"recovered": true}'}]
|
||||||
|
)
|
||||||
server._browsers["avito"] = _FakeSequenceBrowser([racing_page, settled_page])
|
browser = _FakeBrowser(page)
|
||||||
|
server._browsers["avito"] = browser
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
monkeypatch.setattr(server, "FETCH_JSON_LOAD_WAIT_MS", 4242)
|
|
||||||
relaunched = _no_relaunch(monkeypatch)
|
relaunched = _no_relaunch(monkeypatch)
|
||||||
|
|
||||||
result = asyncio.run(
|
result = asyncio.run(
|
||||||
|
|
@ -437,22 +508,21 @@ def test_do_fetch_json_retries_on_destroyed_context(
|
||||||
assert result == {"status": 200, "body": '{"recovered": true}'}
|
assert result == {"status": 200, "body": '{"recovered": true}'}
|
||||||
# Браузер живой — перезапускать его нельзя (тёплые cookies + ~10-20с).
|
# Браузер живой — перезапускать его нельзя (тёплые cookies + ~10-20с).
|
||||||
assert relaunched == []
|
assert relaunched == []
|
||||||
# Первая попытка НЕ ждала load (happy-path не удлиняется), повтор — ждал.
|
# И новой страницы тоже нет: повтор идёт по уже перешедшему контексту.
|
||||||
assert racing_page.load_waits == []
|
assert browser.opened == 1
|
||||||
assert settled_page.load_waits == [4242]
|
assert page.evaluate.await_count == 2
|
||||||
assert racing_page.closed == 1 and settled_page.closed == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_do_fetch_json_retry_survives_load_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_fetch_json_retry_survives_load_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Ожидание load на повторе — best-effort: таймаут не отменяет саму попытку."""
|
"""Ожидание load на повторе — best-effort: таймаут не отменяет саму попытку."""
|
||||||
racing_page = _FakePage({"status": 0, "body": ""})
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
racing_page.evaluate = AsyncMock(side_effect=RuntimeError("Execution context was destroyed"))
|
page.evaluate = AsyncMock(
|
||||||
settled_page = _FakePage({"status": 200, "body": "ok"})
|
side_effect=[RuntimeError("Execution context was destroyed"), {"status": 200, "body": "ok"}]
|
||||||
settled_page.wait_for_load_state = AsyncMock( # type: ignore[method-assign]
|
)
|
||||||
|
page.wait_for_load_state = AsyncMock( # type: ignore[method-assign]
|
||||||
side_effect=TimeoutError("Timeout 15000ms exceeded")
|
side_effect=TimeoutError("Timeout 15000ms exceeded")
|
||||||
)
|
)
|
||||||
|
server._browsers["avito"] = _FakeBrowser(page)
|
||||||
server._browsers["avito"] = _FakeSequenceBrowser([racing_page, settled_page])
|
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
_no_relaunch(monkeypatch)
|
_no_relaunch(monkeypatch)
|
||||||
|
|
||||||
|
|
@ -470,14 +540,18 @@ def test_do_fetch_json_retry_survives_load_timeout(monkeypatch: pytest.MonkeyPat
|
||||||
|
|
||||||
|
|
||||||
def test_do_fetch_json_gives_up_after_one_context_retry(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_do_fetch_json_gives_up_after_one_context_retry(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Повтор ровно один: вторая та же ошибка уезжает наверх, а не крутит цикл."""
|
"""Повтор ровно один: вторая та же ошибка уезжает наверх, а не крутит цикл.
|
||||||
message = "Page.evaluate: Execution context was destroyed"
|
|
||||||
first = _FakePage({"status": 0, "body": ""})
|
|
||||||
first.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
|
||||||
second = _FakePage({"status": 0, "body": ""})
|
|
||||||
second.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
|
||||||
|
|
||||||
server._browsers["avito"] = _FakeSequenceBrowser([first, second])
|
Свежей страницы тут намеренно НЕТ: пересоздание воспроизводит ту же первую
|
||||||
|
навигацию origin'а (прод 06.08 — обе попытки упали одинаково), а страховкой
|
||||||
|
остаётся один повтор всего запроса на стороне клиента (BrowserFetcher).
|
||||||
|
"""
|
||||||
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
|
page.evaluate = AsyncMock(
|
||||||
|
side_effect=RuntimeError("Page.evaluate: Execution context was destroyed")
|
||||||
|
)
|
||||||
|
browser = _FakeBrowser(page)
|
||||||
|
server._browsers["avito"] = browser
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
_no_relaunch(monkeypatch)
|
_no_relaunch(monkeypatch)
|
||||||
|
|
||||||
|
|
@ -492,15 +566,15 @@ def test_do_fetch_json_gives_up_after_one_context_retry(monkeypatch: pytest.Monk
|
||||||
origin="https://www.avito.ru/",
|
origin="https://www.avito.ru/",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
first.evaluate.assert_awaited_once()
|
assert page.evaluate.await_count == 2
|
||||||
second.evaluate.assert_awaited_once()
|
assert browser.opened == 1
|
||||||
|
|
||||||
|
|
||||||
def test_do_fetch_json_does_not_retry_unrelated_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_do_fetch_json_does_not_retry_unrelated_error(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Чужая ошибка НЕ ретраится — повтор невосстановимого жжёт бюджет прогона."""
|
"""Чужая ошибка НЕ ретраится — повтор невосстановимого жжёт бюджет прогона."""
|
||||||
page = _FakePage({"status": 0, "body": ""})
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
page.evaluate = AsyncMock(side_effect=RuntimeError("net::ERR_PROXY_CONNECTION_FAILED"))
|
page.evaluate = AsyncMock(side_effect=RuntimeError("net::ERR_PROXY_CONNECTION_FAILED"))
|
||||||
server._browsers["avito"] = _FakeSequenceBrowser([page])
|
server._browsers["avito"] = _FakeBrowser(page)
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
_no_relaunch(monkeypatch)
|
_no_relaunch(monkeypatch)
|
||||||
|
|
||||||
|
|
@ -521,11 +595,9 @@ def test_do_fetch_json_does_not_retry_unrelated_error(monkeypatch: pytest.Monkey
|
||||||
def test_fetch_json_handler_500_carries_reason_in_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_fetch_json_handler_500_carries_reason_in_body(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
"""Причина отказа остаётся в теле 500 — её читает _raise_for_sidecar_status (#2708)."""
|
"""Причина отказа остаётся в теле 500 — её читает _raise_for_sidecar_status (#2708)."""
|
||||||
message = "Execution context was destroyed"
|
message = "Execution context was destroyed"
|
||||||
first = _FakePage({"status": 0, "body": ""})
|
page = _FakePage({"status": 0, "body": ""})
|
||||||
first.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
page.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
||||||
second = _FakePage({"status": 0, "body": ""})
|
server._browsers["avito"] = _FakeBrowser(page)
|
||||||
second.evaluate = AsyncMock(side_effect=RuntimeError(message))
|
|
||||||
server._browsers["avito"] = _FakeSequenceBrowser([first, second])
|
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
||||||
_no_relaunch(monkeypatch)
|
_no_relaunch(monkeypatch)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue