Compare commits
No commits in common. "5968d7804998c4bd1fc4142def0f48f189a23b0f" and "dfdd97852811653d9505cd7f17daeac8e4eee78a" have entirely different histories.
5968d78049
...
dfdd978528
2 changed files with 14 additions and 97 deletions
|
|
@ -98,19 +98,6 @@ BROWSER_NAV_TIMEOUT_MS: int = int(os.environ.get("BROWSER_NAV_TIMEOUT_MS", "6000
|
||||||
# выдача (~50 карточек, 3.2МБ). Подтверждено прод-дебагом 2026-05-31.
|
# выдача (~50 карточек, 3.2МБ). Подтверждено прод-дебагом 2026-05-31.
|
||||||
BROWSER_WAIT_MS: int = int(os.environ.get("BROWSER_WAIT_MS", "6000"))
|
BROWSER_WAIT_MS: int = int(os.environ.get("BROWSER_WAIT_MS", "6000"))
|
||||||
|
|
||||||
# /fetch-json settle после goto(origin) перед in-page fetch (#1917). 500мс мало:
|
|
||||||
# первый XHR иногда ловит `NetworkError when attempting to fetch resource` (anti-bot/
|
|
||||||
# сетевой стек страницы ещё не готов). Лечился внешним retry (re-navigation ~30-45с/дом).
|
|
||||||
# Поднимаем дефолт до 1200мс (меньше first-fail) + добавляем дешёвый in-page retry
|
|
||||||
# самого fetch() (см. FETCH_JSON_INPAGE_RETRIES) — повтор внутри страницы стоит ~доли
|
|
||||||
# секунды против полной ре-навигации. НЕ трогаем /fetch (SERP): там свой BROWSER_WAIT_MS.
|
|
||||||
FETCH_JSON_SETTLE_MS: int = int(os.environ.get("FETCH_JSON_SETTLE_MS", "1200"))
|
|
||||||
# Сколько раз повторить in-page fetch() при сетевом throw (TypeError/NetworkError),
|
|
||||||
# НЕ при HTTP-статусе (4xx/5xx возвращаются как есть — их решает caller). 0 = выкл.
|
|
||||||
FETCH_JSON_INPAGE_RETRIES: int = int(os.environ.get("FETCH_JSON_INPAGE_RETRIES", "1"))
|
|
||||||
# Пауза между in-page попытками fetch(), мс.
|
|
||||||
FETCH_JSON_RETRY_DELAY_MS: int = int(os.environ.get("FETCH_JSON_RETRY_DELAY_MS", "800"))
|
|
||||||
|
|
||||||
# Известные поставщики. "generic" — фолбэк для всех прочих хостов (один общий
|
# Известные поставщики. "generic" — фолбэк для всех прочих хостов (один общий
|
||||||
# инстанс на неузнанные домены). Порядок задаёт детерминированный health-вывод.
|
# инстанс на неузнанные домены). Порядок задаёт детерминированный health-вывод.
|
||||||
PROVIDERS: tuple[str, ...] = ("avito", "cian", "yandex", "generic")
|
PROVIDERS: tuple[str, ...] = ("avito", "cian", "yandex", "generic")
|
||||||
|
|
@ -825,44 +812,21 @@ async def _fetch_json_once(
|
||||||
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]
|
||||||
# БЕЗ полного BROWSER_WAIT_MS: нам нужен лишь origin-контекст (cookies +
|
# БЕЗ полного BROWSER_WAIT_MS: нам нужен лишь origin-контекст (cookies +
|
||||||
# same-origin scope для fetch), а не отрендеренные listings. Settle-паузы
|
# same-origin scope для fetch), а не отрендеренные listings. Короткой паузы
|
||||||
# (#1917, FETCH_JSON_SETTLE_MS) хватает, чтобы страница инициализировалась
|
# хватает, чтобы страница инициализировалась перед in-page fetch'ем.
|
||||||
# перед in-page fetch'ем.
|
await page.wait_for_timeout(500) # type: ignore[attr-defined]
|
||||||
await page.wait_for_timeout(FETCH_JSON_SETTLE_MS) # type: ignore[attr-defined]
|
|
||||||
# In-page retry самого fetch() при СЕТЕВОМ throw (TypeError/"NetworkError"),
|
|
||||||
# #1917: первый XHR после навигации иногда падает до готовности стека. Повтор
|
|
||||||
# внутри страницы (~retryDelayMs) дешевле полной ре-навигации (~30-45с/дом).
|
|
||||||
# HTTP-статусы (4xx/5xx) — это успешный resp, НЕ ретраим (решает caller).
|
|
||||||
result: dict = await page.evaluate( # type: ignore[attr-defined]
|
result: dict = await page.evaluate( # type: ignore[attr-defined]
|
||||||
"""async ({url, method, headers, body, retries, retryDelayMs}) => {
|
"""async ({url, method, headers, body}) => {
|
||||||
let lastErr;
|
const resp = await fetch(url, {
|
||||||
for (let attempt = 0; attempt <= retries; attempt++) {
|
method: method || 'GET',
|
||||||
try {
|
headers: headers || {},
|
||||||
const resp = await fetch(url, {
|
body: (body !== null && body !== undefined) ? body : undefined,
|
||||||
method: method || 'GET',
|
credentials: 'include',
|
||||||
headers: headers || {},
|
});
|
||||||
body: (body !== null && body !== undefined) ? body : undefined,
|
const text = await resp.text();
|
||||||
credentials: 'include',
|
return { status: resp.status, body: text };
|
||||||
});
|
|
||||||
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;
|
|
||||||
}""",
|
}""",
|
||||||
{
|
{"url": url, "method": method, "headers": headers or {}, "body": body},
|
||||||
"url": url,
|
|
||||||
"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]
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,6 @@ class _FakePage:
|
||||||
|
|
||||||
def __init__(self, evaluate_result: dict[str, Any]) -> None:
|
def __init__(self, evaluate_result: dict[str, Any]) -> None:
|
||||||
self.goto_urls: list[str] = []
|
self.goto_urls: list[str] = []
|
||||||
self.waits: list[int] = [] # записанные wait_for_timeout(ms) — settle-проверка #1917
|
|
||||||
self.closed = 0
|
self.closed = 0
|
||||||
# evaluate — AsyncMock, чтобы проверять как сам результат, так и аргументы.
|
# evaluate — AsyncMock, чтобы проверять как сам результат, так и аргументы.
|
||||||
self.evaluate = AsyncMock(return_value=evaluate_result)
|
self.evaluate = AsyncMock(return_value=evaluate_result)
|
||||||
|
|
@ -78,7 +77,7 @@ class _FakePage:
|
||||||
self.goto_urls.append(url)
|
self.goto_urls.append(url)
|
||||||
|
|
||||||
async def wait_for_timeout(self, ms: int) -> None:
|
async def wait_for_timeout(self, ms: int) -> None:
|
||||||
self.waits.append(ms)
|
return None
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
self.closed += 1
|
self.closed += 1
|
||||||
|
|
@ -206,52 +205,6 @@ def test_fetch_json_passes_post_method_and_body(monkeypatch: pytest.MonkeyPatch)
|
||||||
assert payload["body"] == '{"a": 1}'
|
assert payload["body"] == '{"a": 1}'
|
||||||
|
|
||||||
|
|
||||||
# ── settle + in-page retry (#1917) ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_json_settle_uses_configured_ms(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
"""Settle перед in-page fetch = FETCH_JSON_SETTLE_MS (а не хардкод 500)."""
|
|
||||||
page = _FakePage({"status": 200, "body": "ok"})
|
|
||||||
server._browsers["avito"] = _FakeBrowser(page)
|
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
|
||||||
monkeypatch.setattr(server, "FETCH_JSON_SETTLE_MS", 1777)
|
|
||||||
|
|
||||||
async def _ensure(provider: str) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
monkeypatch.setattr(server, "_ensure_browser", _ensure)
|
|
||||||
|
|
||||||
asyncio.run(
|
|
||||||
server.fetch_json_handler(
|
|
||||||
_make_request({"url": "https://www.avito.ru/api/x", "source": "avito"})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert page.waits == [1777]
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_json_passes_inpage_retry_params(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
||||||
"""В page.evaluate прокидываются retries/retryDelayMs из конфигурации (#1917)."""
|
|
||||||
page = _FakePage({"status": 200, "body": "ok", "attempts": 1})
|
|
||||||
server._browsers["avito"] = _FakeBrowser(page)
|
|
||||||
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)
|
|
||||||
monkeypatch.setattr(server, "FETCH_JSON_INPAGE_RETRIES", 2)
|
|
||||||
monkeypatch.setattr(server, "FETCH_JSON_RETRY_DELAY_MS", 654)
|
|
||||||
|
|
||||||
async def _ensure(provider: str) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
monkeypatch.setattr(server, "_ensure_browser", _ensure)
|
|
||||||
|
|
||||||
asyncio.run(
|
|
||||||
server.fetch_json_handler(
|
|
||||||
_make_request({"url": "https://www.avito.ru/api/x", "source": "avito"})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
payload = page.evaluate.await_args.args[1]
|
|
||||||
assert payload["retries"] == 2
|
|
||||||
assert payload["retryDelayMs"] == 654
|
|
||||||
|
|
||||||
|
|
||||||
# ── валидация / resilience ────────────────────────────────────────────────────────
|
# ── валидация / resilience ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue