fix(tradein/browser): гонка «execution context destroyed» — восстановимая, ретрай без relaunch (#2676) #2716

Merged
bot-backend merged 1 commit from fix/2676-sidecar-recoverable into main 2026-08-06 09:44:23 +00:00
Collaborator

Что чинит

/fetch-json сайдкара: goto(origin, domcontentloaded) → фиксированный settle → page.evaluate. Страница успевает уйти в клиентскую навигацию, playwright рвёт JS-контекст → Execution context was destroyed. Эта ошибка не попадала ни в одну ветку восстановления (_is_browser_crash знает только закрытие цели/браузера/соединения) → 500 наверх. Сайдкар общий, поэтому радиус — все детальные проходы (avito/cian/domclick).

Замеры с прода (что подтвердилось, что нет)

факт число
прогон house_imv_backfill 05.08 checked=50, errors=35, skipped=15, saved=0
из них отказ сайдкара 500 на /fetch-json 23 дома (houses.imv_error_reason, 05.08)
накопительно домов с этим же 500 83 (последний — 05.08 17:14)
403 самой площадки в том же прогоне 12

Опровергнутая предпосылка №1 (важная). Из issue: «23 отказа по причине execution context». Проверить это уже НЕЛЬЗЯ: причина отказа до #2708 выбрасывалась клиентом (в БД лежит только «Server error '500 Internal Server Error' for url .../fetch-json»), а логи сайдкара за 05.08 стёрты пересозданием контейнера (tradein-browser создан 06.08 03:43 MSK). В живом окне логов (с 00:44 UTC 06.08) строки «execution context» 0, а единственный 500 с видимой причиной — NS_ERROR_PROXY_BAD_GATEWAY (04:12, домклик), т.е. тот же код отказа даёт как минимум две разные причины. Число 23 — это счётчик 500-х, а не этой конкретной гонки.

Опровергнутая предпосылка №2. «Ретрая не происходит» — на клиенте он есть: BrowserFetcher.fetch_json ловит httpx.HTTPErrorHTTPStatusError ⊂ HTTPError) и делает один повтор. Наблюдение из issue «один и тот же адрес падает дважды подряд» — это и есть попытка + повтор. Значит повтор «в лоб» уже был испробован и НЕ помог → гонка воспроизводится, а не рассасывается. Поэтому здесь повтор не просто добавлен, а сделан другим: с ожиданием load.

Что сделано

  1. _is_page_context_lost — отдельный признак, не смешанный с крашем браузера. Маркер взят из самого playwright (driver 1.60.0, coreBundle.js в образе сайдкара — проверено grep'ом в живом контейнере), а не из строки в нашем логе; обе формы сообщения имеют общий префикс.
  2. Повтор на свежей странице без _relaunch_browser: браузер жив, умерла одна страница — relaunch стоил бы ~10-20с и тёплый профиль инстанса.
  3. Повтор ждёт load (FETCH_JSON_LOAD_WAIT_MS, best-effort, таймаут не отменяет попытку). Happy-path не удлиняется — ожидание только на ретрае.
  4. Повтор строго один и только по этому признаку: чужая ошибка уезжает наверх сразу.

Чего тут НЕТ

Вариант 2 из issue («ждать load всегда») не берётся: без замера доли этой ошибки по другим потребителям (в живом окне — 0) удлинять КАЖДЫЙ /fetch-json ради гипотезы нельзя. Ожидание включено ровно там, где ошибка уже случилась.

Test plan

  • test_server_fetch_json.py: обе формы сообщения playwright → повтор без relaunch, load ждётся только на повторе; таймаут load не отменяет попытку; повтор ровно один; чужая ошибка не ретраится; причина остаётся в теле 500 (стык с #2708)
  • 17 passed локально (CI сюда не доезжает: ci-tradein.yml гоняет только tradein-mvp/backend; tradein-mvp/browser/** тестов в пайплайне нет — отдельная дыра, не этого PR)
  • на коде до правки 4 из 6 новых тестов падают

Refs #2676

## Что чинит `/fetch-json` сайдкара: `goto(origin, domcontentloaded)` → фиксированный settle → `page.evaluate`. Страница успевает уйти в клиентскую навигацию, playwright рвёт JS-контекст → `Execution context was destroyed`. Эта ошибка не попадала ни в одну ветку восстановления (`_is_browser_crash` знает только закрытие цели/браузера/соединения) → 500 наверх. Сайдкар общий, поэтому радиус — все детальные проходы (avito/cian/domclick). ## Замеры с прода (что подтвердилось, что нет) | факт | число | |---|---| | прогон `house_imv_backfill` 05.08 | `checked=50, errors=35, skipped=15, saved=0` | | из них отказ сайдкара 500 на `/fetch-json` | **23 дома** (`houses.imv_error_reason`, 05.08) | | накопительно домов с этим же 500 | 83 (последний — 05.08 17:14) | | 403 самой площадки в том же прогоне | 12 | **Опровергнутая предпосылка №1 (важная).** Из issue: «23 отказа по причине execution context». Проверить это уже НЕЛЬЗЯ: причина отказа до #2708 выбрасывалась клиентом (в БД лежит только «Server error '500 Internal Server Error' for url .../fetch-json»), а логи сайдкара за 05.08 стёрты пересозданием контейнера (`tradein-browser` создан 06.08 03:43 MSK). В живом окне логов (с 00:44 UTC 06.08) строки «execution context» **0**, а единственный 500 с видимой причиной — `NS_ERROR_PROXY_BAD_GATEWAY` (04:12, домклик), т.е. тот же код отказа даёт как минимум две разные причины. Число 23 — это счётчик 500-х, а не этой конкретной гонки. **Опровергнутая предпосылка №2.** «Ретрая не происходит» — на клиенте он есть: `BrowserFetcher.fetch_json` ловит `httpx.HTTPError` (а `HTTPStatusError ⊂ HTTPError`) и делает один повтор. Наблюдение из issue «один и тот же адрес падает дважды подряд» — это и есть попытка + повтор. Значит повтор «в лоб» уже был испробован и НЕ помог → гонка воспроизводится, а не рассасывается. Поэтому здесь повтор не просто добавлен, а сделан другим: с ожиданием `load`. ## Что сделано 1. `_is_page_context_lost` — отдельный признак, не смешанный с крашем браузера. Маркер взят из самого playwright (driver 1.60.0, `coreBundle.js` в образе сайдкара — проверено grep'ом в живом контейнере), а не из строки в нашем логе; обе формы сообщения имеют общий префикс. 2. Повтор на свежей странице **без** `_relaunch_browser`: браузер жив, умерла одна страница — relaunch стоил бы ~10-20с и тёплый профиль инстанса. 3. Повтор ждёт `load` (`FETCH_JSON_LOAD_WAIT_MS`, best-effort, таймаут не отменяет попытку). Happy-path не удлиняется — ожидание только на ретрае. 4. Повтор строго один и только по этому признаку: чужая ошибка уезжает наверх сразу. ## Чего тут НЕТ Вариант 2 из issue («ждать `load` всегда») не берётся: без замера доли этой ошибки по другим потребителям (в живом окне — 0) удлинять КАЖДЫЙ `/fetch-json` ради гипотезы нельзя. Ожидание включено ровно там, где ошибка уже случилась. ## Test plan - [x] `test_server_fetch_json.py`: обе формы сообщения playwright → повтор без relaunch, `load` ждётся только на повторе; таймаут `load` не отменяет попытку; повтор ровно один; чужая ошибка не ретраится; причина остаётся в теле 500 (стык с #2708) - [x] 17 passed локально (CI сюда не доезжает: `ci-tradein.yml` гоняет только `tradein-mvp/backend`; `tradein-mvp/browser/**` тестов в пайплайне нет — отдельная дыра, не этого PR) - [x] на коде до правки 4 из 6 новых тестов падают Refs #2676
bot-backend added 1 commit 2026-08-06 09:36:27 +00:00
fix(tradein/browser): гонка «execution context destroyed» — восстановимая, ретрай без relaunch (#2676)
All checks were successful
CI / changes (pull_request) Successful in 7s
CI Trade-In / changes (pull_request) Successful in 7s
CI Trade-In / backend-tests (pull_request) Has been skipped
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
f218f85585
`_fetch_json_once` делает goto(origin, domcontentloaded) → фиксированный settle →
page.evaluate. Если страница за это время уходит в клиентскую навигацию, playwright
рвёт JS-контекст и бросает «Execution context was destroyed». Такая ошибка не
попадала ни в одну ветку восстановления: `_is_browser_crash` матчит только закрытие
цели/браузера/соединения — значит для сайдкара это окончательный отказ, 500 наверх.

Правка:

1. `_is_page_context_lost` — отдельный признак, не смешанный с крашем браузера.
   Маркер взят из САМОГО playwright (driver 1.60.0, coreBundle.js в образе
   сайдкара), а не из строки в нашем логе: обе формы сообщения — короткая и
   «..., most likely because of a navigation.» — имеют общий префикс.

2. Повтор на СВЕЖЕЙ странице без `_relaunch_browser`: браузер жив, умерла одна
   страница. Relaunch стоил бы ~10-20с и тёплый профиль инстанса (cookies,
   фингерпринт) ради здорового браузера.

3. Повтор ждёт `load` (FETCH_JSON_LOAD_WAIT_MS, best-effort). Без этого повтор
   попадает ровно в то же окно навигации: в логе один и тот же адрес падал дважды
   подряд, то есть гонка воспроизводится, а не «рассасывается». Happy-path не
   удлиняется — ожидание только на ретрае.

Ретрай строго один и только по этому признаку: чужая ошибка (напр.
ERR_PROXY_CONNECTION_FAILED) уезжает наверх сразу — повтор невосстановимого стоит
полной ре-навигации на каждый дом.

Refs #2676
bot-backend merged commit 1ba1a55770 into main 2026-08-06 09:44:23 +00:00
bot-backend deleted branch fix/2676-sidecar-recoverable 2026-08-06 09:44:24 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#2716
No description provided.