fix(tradein/domclick): каждый запрос получал чистый браузерный контекст, куки реплеились протухшими (#3190) #3193

Merged
lekss361 merged 1 commit from fix/3118-domclick-warm-context into main 2026-08-28 19:02:32 +00:00
Owner

Закрывает корневую причину из #3190. Refs #3118.

Что было

domclick_detail_backfill две недели подряд давал attempted=4, enriched=1, blocked=3 и статус banned. Выглядело как «площадка нас забанила», оказалось иначе.

Сайдкар на каждый /fetch делает page = await browser.new_page() поверх AsyncCamoufox. _browsers[provider] — это Browser, а не персистентный контекст, значит каждый запрос получал новый изолированный контекст, и cookie-jar умирал сразу после ответа. Туда вливался замороженный снимок кук из domclick_session_cookies, в котором лежит qrator_jsid2 — токен со сроком жизни ~2.5 часа, хранящийся у нас 30 дней.

Первый запрос после заливки кук проходил с ещё живым токеном. Площадка выдавала новый через Set-Cookie, мы его выбрасывали вместе с контекстом и на втором запросе показывали тот же протухший. QRATOR отвечал челлендж-страницей без __SSR_STATE__.

Замеры на проде, 28.08

Свежий аккаунт залит, сессия валидна, куки доезжают до боевого пути (last_used_at совпадает с моментом старта прогона).

Условие Результат
первая карточка после заливки кук OKitem_id, year_built, история цены
12 карточек подряд 12 блоков из 12
6 карточек, аренда узлов из пула (1 → 9) 6 из 6
8 карточек, узел 10 со свежим exit-IP после ротации 8 из 8, блок с первого запроса
те же 5 карточек в тёплом контексте 5 из 5 успешно, ~2 сек каждая

Свежий exit-IP блокируется мгновенно — репутация адреса ни при чём. Тёплый контекст на тех же карточках проходит полностью.

Что сделано

  • Сайдкар держит переиспользуемый per-provider browser.new_context(). Куки вливаются только при создании контекста, дальше jar живёт сам. Страница закрывается после ответа, контекст остаётся.
  • Закрытие контекста подшито к _close_browser, поэтому crash-relaunch, recycle-relaunch и shutdown его не теряют — висящих контекстов у мёртвого браузера не остаётся.
  • Флаг opt-in. При reuse_context=False (дефолт) payload /fetch не получает новых ключей вовсе — поведение остальных трёх поставщиков не меняется ни на байт.
  • Сброс сожжённого контекста — ровно один на обнаруженный блок, через отложенный флаг BrowserFetcher.request_context_reset(). Понадобился потому, что fetch_detail() в kit не прокидывает reset_context, а менять этот промежуточный слой ради одного поставщика в рамках фикса не хотелось.

⚠️ Порядок деплоя

Правка работает, только если tradein-browser пересобран вместе с бэкендом. Старый сайдкар новых полей не знает: не упадёт, но и не починит ничего — продолжит делать new_page() на каждый запрос. Если после мержа метрика не сдвинулась, первым делом проверять, что контейнер браузера действительно пересоздан.

Проверки

  • tradein-mvp/browser/ — 137 тестов зелёные, включая новый test_server_reuse_context.py (8 тестов: контекст создаётся один раз на несколько фетчей, куки вливаются только при создании, reset_context выбрасывает старый, при reuse_context=False поведение прежнее, _close_browser закрывает контекст).
  • test_3118_domclick_warm_context.py — 7 тестов: оркестратор зовёт сброс ровно раз на блок; отложенный флаг потребляется ровно одним следующим fetch(); переживает transport-retry; при выключенном флаге payload чист.
  • tests/tasks/test_domclick_detail_backfill.py — 10 тестов зелёные.
  • ruff чист по всем изменённым файлам.

Ревью пройдено (⚠️ minor, блокеров нет), оба замечания закрыты в этой же ветке: дублирование сброса на retry явно задокументировано как осознанная идемпотентность, и добавлены три теста на саму логику флага в BrowserFetcher — раньше она не исполнялась ни одним тестом.

Приёмка после деплоя

SELECT id, started_at, status, counters
FROM scrape_runs WHERE source='domclick_detail_backfill'
ORDER BY started_at DESC LIMIT 5;

База: enriched 0-2 за прогон, blocked=3, статус banned — четырнадцать прогонов подряд. Успех — enriched заметно больше при том же blocked, в идеале прогон доходит до конца бюджета вместо остановки по третьему блоку. В очереди 1796 активных объявлений без обогащения, так что материала для замера достаточно.

Закрывает корневую причину из #3190. Refs #3118. ## Что было `domclick_detail_backfill` две недели подряд давал `attempted=4, enriched=1, blocked=3` и статус `banned`. Выглядело как «площадка нас забанила», оказалось иначе. Сайдкар на каждый `/fetch` делает `page = await browser.new_page()` поверх `AsyncCamoufox`. `_browsers[provider]` — это Browser, а не персистентный контекст, значит **каждый запрос получал новый изолированный контекст**, и cookie-jar умирал сразу после ответа. Туда вливался замороженный снимок кук из `domclick_session_cookies`, в котором лежит `qrator_jsid2` — токен со сроком жизни **~2.5 часа**, хранящийся у нас **30 дней**. Первый запрос после заливки кук проходил с ещё живым токеном. Площадка выдавала новый через `Set-Cookie`, мы его выбрасывали вместе с контекстом и на втором запросе показывали тот же протухший. QRATOR отвечал челлендж-страницей без `__SSR_STATE__`. ## Замеры на проде, 28.08 Свежий аккаунт залит, сессия валидна, куки доезжают до боевого пути (`last_used_at` совпадает с моментом старта прогона). | Условие | Результат | |---|---| | первая карточка после заливки кук | **OK** — `item_id`, `year_built`, история цены | | 12 карточек подряд | 12 блоков из 12 | | 6 карточек, аренда узлов из пула (1 → 9) | 6 из 6 | | 8 карточек, узел 10 **со свежим exit-IP после ротации** | 8 из 8, блок с первого запроса | | **те же 5 карточек в тёплом контексте** | **5 из 5 успешно**, ~2 сек каждая | Свежий exit-IP блокируется мгновенно — репутация адреса ни при чём. Тёплый контекст на тех же карточках проходит полностью. ## Что сделано - Сайдкар держит переиспользуемый per-provider `browser.new_context()`. Куки вливаются **только при создании** контекста, дальше jar живёт сам. Страница закрывается после ответа, контекст остаётся. - Закрытие контекста подшито к `_close_browser`, поэтому crash-relaunch, recycle-relaunch и shutdown его не теряют — висящих контекстов у мёртвого браузера не остаётся. - Флаг opt-in. При `reuse_context=False` (дефолт) payload `/fetch` не получает новых ключей **вовсе** — поведение остальных трёх поставщиков не меняется ни на байт. - Сброс сожжённого контекста — ровно один на обнаруженный блок, через отложенный флаг `BrowserFetcher.request_context_reset()`. Понадобился потому, что `fetch_detail()` в kit не прокидывает `reset_context`, а менять этот промежуточный слой ради одного поставщика в рамках фикса не хотелось. ## ⚠️ Порядок деплоя Правка работает, **только если `tradein-browser` пересобран вместе с бэкендом**. Старый сайдкар новых полей не знает: не упадёт, но и не починит ничего — продолжит делать `new_page()` на каждый запрос. Если после мержа метрика не сдвинулась, первым делом проверять, что контейнер браузера действительно пересоздан. ## Проверки - `tradein-mvp/browser/` — 137 тестов зелёные, включая новый `test_server_reuse_context.py` (8 тестов: контекст создаётся один раз на несколько фетчей, куки вливаются только при создании, `reset_context` выбрасывает старый, при `reuse_context=False` поведение прежнее, `_close_browser` закрывает контекст). - `test_3118_domclick_warm_context.py` — 7 тестов: оркестратор зовёт сброс ровно раз на блок; отложенный флаг потребляется ровно одним следующим `fetch()`; переживает transport-retry; при выключенном флаге payload чист. - `tests/tasks/test_domclick_detail_backfill.py` — 10 тестов зелёные. - ruff чист по всем изменённым файлам. Ревью пройдено (⚠️ minor, блокеров нет), оба замечания закрыты в этой же ветке: дублирование сброса на retry явно задокументировано как осознанная идемпотентность, и добавлены три теста на саму логику флага в `BrowserFetcher` — раньше она не исполнялась ни одним тестом. ## Приёмка после деплоя ```sql SELECT id, started_at, status, counters FROM scrape_runs WHERE source='domclick_detail_backfill' ORDER BY started_at DESC LIMIT 5; ``` База: `enriched` 0-2 за прогон, `blocked=3`, статус `banned` — четырнадцать прогонов подряд. Успех — `enriched` заметно больше при том же `blocked`, в идеале прогон доходит до конца бюджета вместо остановки по третьему блоку. В очереди 1796 активных объявлений без обогащения, так что материала для замера достаточно.
bot-backend added 1 commit 2026-08-28 18:44:31 +00:00
fix(tradein/domclick): каждый запрос получал чистый браузерный контекст, куки реплеились протухшими
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / changes (pull_request) Successful in 9s
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 / browser-tests (pull_request) Successful in 1m7s
CI Trade-In / backend-tests (pull_request) Successful in 4m49s
e2863f6be9
Сайдкар на каждый /fetch делал browser.new_page() поверх AsyncCamoufox — это новый
изолированный контекст, cookie-jar умирал сразу после ответа. Туда вливался
замороженный снимок кук из domclick_session_cookies, где qrator_jsid2 живёт ~2.5
часа, а хранится 30 дней. Первый запрос проходил с ещё живым токеном, все следующие
показывали QRATOR один и тот же протухший — он отдавал челлендж-страницу без
__SSR_STATE__. Отсюда двухнедельное attempted=4, enriched=1, blocked=3.

Замер на проде 28.08: через сайдкар 26 запросов подряд — 100% блоков, включая
свежеротированный exit-IP; те же карточки в тёплом контексте — 5 из 5 успешно,
~2 сек каждая.

Сайдкар получил переиспользуемый per-provider контекст: куки вливаются один раз при
создании, дальше jar живёт сам; страница закрывается после ответа, контекст остаётся.
Закрытие контекста подшито к _close_browser, так что relaunch и shutdown его не
теряют. Флаг opt-in: при reuse_context=False payload /fetch не получает новых ключей
вовсе, поведение остальных поставщиков не меняется.

Сброс сожжённого контекста — ровно один на обнаруженный блок, через отложенный флаг
BrowserFetcher.request_context_reset(): fetch_detail() в kit не прокидывает
reset_context, и менять этот промежуточный слой ради одного поставщика не хотелось.

NB для деплоя: правка работает только если tradein-browser пересобран вместе с
бэкендом. Старый сайдкар новые поля молча проигнорирует — не упадёт, но и не починит.

Refs #3190, #3118
lekss361 merged commit 0ac443d0ba into main 2026-08-28 19:02:32 +00:00
lekss361 deleted branch fix/3118-domclick-warm-context 2026-08-28 19:02:32 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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#3193
No description provided.