fix(tradein/domclick): одна карточка на процесс браузера — перезапускать camoufox после каждой страницы (#3205) #3206

Merged
lekss361 merged 1 commit from fix/3205-domclick-recycle into main 2026-08-29 10:16:56 +00:00
Owner

Closes #3205.

Причина

ДомКлик режет не IP, не сессию и не челлендж. Он отдаёт ровно одну карточку на запущенный экземпляр браузера; вторая и далее получают страницу отказа. Прод держал один браузер на весь прогон — отсюда «примерно одна карточка за прогон» все эти месяцы.

Замеры (прод-сервер, 2026-08-29)

Три независимых запуска camoufox через прод-прокси, идём до первого отказа:

--- запуск 1: браузер поднялся за 3.5 с ---
    карточка #1  http=200   len=  968674  OK
    карточка #2  http=401   len=   26625  ОТКАЗ
--- запуск 2: браузер поднялся за 0.6 с ---
    карточка #1  http=200   len= 1003055  OK
    карточка #2  http=401   len=   26625  ОТКАЗ
--- запуск 3: браузер поднялся за 0.6 с ---
    карточка #1  http=200   len= 1030764  OK
    карточка #2  http=401   len=   26625  ОТКАЗ
условие успехов
свой браузер на каждую карточку 4 из 4
один браузер на четыре карточки 1 из 4
прямой выход с сервера, один браузер 1 из 8
через прод-прокси, один браузер 1 из 8

Прокси и прямой выход неразличимы — дело не в точке выхода. Цена перезапуска — 0.6 с.

Свежего контекста не хватает. В контрольном замере каждая карточка бралась через browser.new_page(), то есть в новом изолированном контексте, — и всё равно отказ со второй. Признак живёт на уровне процесса: camoufox генерирует отпечаток при запуске, а не при создании контекста. Значит reset_context из #3118 эту задачу не решает в принципе.

Что сделано

  • PROVIDERS пополнился "domclick" (плюс host-detect domclick.ru → domclick). Раньше он проваливался в generic, деля браузер и счётчик страниц с прочим трафиком, — при пороге перезапуска 1 это было бы просто неверно. Устаревший комментарий в _resolve_provider («generic-путь эквивалентен») переписан.
  • Порог перезапуска стал поставщик-зависимым: _resolve_recycle_pages + BROWSER_RECYCLE_PAGES_{PROVIDER}, по образцу BROWSER_BLOCK_IMAGES_{PROVIDER} (#3185) и BROWSER_MIN_PAGE_INTERVAL_S_{PROVIDER}. Приоритет: per-provider env → глобальный env → код-дефолт per-provider → 15. Невалидное значение — фолбэк, а не падение; <1 приводится к 1.
  • Код-дефолт domclick = 1, остальным прежние 15. Авито и Циан не трогаем: у них узор другой, менять его без своего замера нельзя.

Отдельно: починены ~24 холостых охранника в тестах

По набору стояло monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000) — так тесты запрещали перезапуск браузера. После перехода на словарь этот патч перестал влиять на что-либо: сравнения читают _RECYCLE_PAGES_BY_PROVIDER. Набор оставался зелёным только потому, что ни один тест не делает 15 страниц подряд, — защита держалась на совпадении.

Теперь патчится _RECYCLE_PAGES_BY_PROVIDER, а сама глобальная константа убрана, чтобы её не патчили снова (env-имя BROWSER_RECYCLE_PAGES продолжает читаться внутри резолвера как один из уровней приоритета).

Проверено мутацией: при _RECYCLE_PAGES_DEFAULT_FALLBACK = 1 падают ровно три теста, которые сам этот дефолт и проверяют, а остальные 155 удерживаются — значит охранники работают, а не просто присутствуют.

Проверка

  • tradein-mvp/browser: 158 passed (было 149; +8 новых на _resolve_recycle_pages, +1 на host-detect)
  • Мутационная проба описана выше

Ожидаемый эффект

domclick_detail_backfill (ближайший прогон — сегодня 17:47 UTC) перестаёт упираться в потолок одной карточки. За 14 дней у него 14 прогонов из 14 в banned при нуле обогащений; ограничением станет темп, а не отпечаток браузера.

Что этим опровергнуто

Четыре гипотезы, каждая закрыта замером: протухший qrator_jsid2 (по ней был собран и задеплоен #3193 — не изменил ничего); «camoufox не проходит JS-челлендж» (челленджа в отказе нет вовсе); «выходим через забракованный узел пула» (все четыре узла дают тот же отказ); «виновата сохранённая сессия» (401 одинаково с 16 куками и анонимно).

Closes #3205. ## Причина ДомКлик режет не IP, не сессию и не челлендж. Он отдаёт **ровно одну карточку на запущенный экземпляр браузера**; вторая и далее получают страницу отказа. Прод держал один браузер на весь прогон — отсюда «примерно одна карточка за прогон» все эти месяцы. ## Замеры (прод-сервер, 2026-08-29) Три независимых запуска camoufox через прод-прокси, идём до первого отказа: ``` --- запуск 1: браузер поднялся за 3.5 с --- карточка #1 http=200 len= 968674 OK карточка #2 http=401 len= 26625 ОТКАЗ --- запуск 2: браузер поднялся за 0.6 с --- карточка #1 http=200 len= 1003055 OK карточка #2 http=401 len= 26625 ОТКАЗ --- запуск 3: браузер поднялся за 0.6 с --- карточка #1 http=200 len= 1030764 OK карточка #2 http=401 len= 26625 ОТКАЗ ``` | условие | успехов | |---|---| | свой браузер на каждую карточку | **4 из 4** | | один браузер на четыре карточки | 1 из 4 | | прямой выход с сервера, один браузер | 1 из 8 | | через прод-прокси, один браузер | 1 из 8 | Прокси и прямой выход неразличимы — дело не в точке выхода. Цена перезапуска — **0.6 с**. **Свежего контекста не хватает.** В контрольном замере каждая карточка бралась через `browser.new_page()`, то есть в новом изолированном контексте, — и всё равно отказ со второй. Признак живёт на уровне процесса: camoufox генерирует отпечаток при запуске, а не при создании контекста. Значит `reset_context` из #3118 эту задачу не решает в принципе. ## Что сделано - **`PROVIDERS` пополнился `"domclick"`** (плюс host-detect `domclick.ru → domclick`). Раньше он проваливался в `generic`, деля браузер и счётчик страниц с прочим трафиком, — при пороге перезапуска 1 это было бы просто неверно. Устаревший комментарий в `_resolve_provider` («generic-путь эквивалентен») переписан. - **Порог перезапуска стал поставщик-зависимым**: `_resolve_recycle_pages` + `BROWSER_RECYCLE_PAGES_{PROVIDER}`, по образцу `BROWSER_BLOCK_IMAGES_{PROVIDER}` (#3185) и `BROWSER_MIN_PAGE_INTERVAL_S_{PROVIDER}`. Приоритет: per-provider env → глобальный env → код-дефолт per-provider → 15. Невалидное значение — фолбэк, а не падение; `<1` приводится к 1. - **Код-дефолт `domclick = 1`**, остальным прежние 15. Авито и Циан не трогаем: у них узор другой, менять его без своего замера нельзя. ## Отдельно: починены ~24 холостых охранника в тестах По набору стояло `monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000)` — так тесты запрещали перезапуск браузера. После перехода на словарь этот патч перестал влиять на что-либо: сравнения читают `_RECYCLE_PAGES_BY_PROVIDER`. Набор оставался зелёным только потому, что ни один тест не делает 15 страниц подряд, — защита держалась на совпадении. Теперь патчится `_RECYCLE_PAGES_BY_PROVIDER`, а сама глобальная константа **убрана**, чтобы её не патчили снова (env-имя `BROWSER_RECYCLE_PAGES` продолжает читаться внутри резолвера как один из уровней приоритета). **Проверено мутацией:** при `_RECYCLE_PAGES_DEFAULT_FALLBACK = 1` падают ровно три теста, которые сам этот дефолт и проверяют, а остальные 155 удерживаются — значит охранники работают, а не просто присутствуют. ## Проверка - `tradein-mvp/browser`: **158 passed** (было 149; +8 новых на `_resolve_recycle_pages`, +1 на host-detect) - Мутационная проба описана выше ## Ожидаемый эффект `domclick_detail_backfill` (ближайший прогон — сегодня 17:47 UTC) перестаёт упираться в потолок одной карточки. За 14 дней у него 14 прогонов из 14 в `banned` при нуле обогащений; ограничением станет темп, а не отпечаток браузера. ## Что этим опровергнуто Четыре гипотезы, каждая закрыта замером: протухший `qrator_jsid2` (по ней был собран и задеплоен #3193 — не изменил ничего); «camoufox не проходит JS-челлендж» (челленджа в отказе нет вовсе); «выходим через забракованный узел пула» (все четыре узла дают тот же отказ); «виновата сохранённая сессия» (401 одинаково с 16 куками и анонимно).
lekss361 added 1 commit 2026-08-29 10:15:11 +00:00
fix(tradein/domclick): площадка отдаёт одну карточку на процесс браузера, а мы держали один на весь прогон (#3205)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 10s
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
CI Trade-In / browser-tests (pull_request) Successful in 1m5s
b9025de666
Замер на проде 2026-08-29. Три независимых запуска camoufox через прод-прокси,
идём по карточкам до первого отказа — каждый раз одно и то же: карточка #1
даёт 200 и ~1 МБ с SSR-стейтом, карточка #2 даёт 401 и страницу отказа на
26 625 байт. A/B на восьми карточках: свой браузер на каждую — 4 из 4 успеха,
один браузер на четыре — 1 из 4. Прямой выход с сервера и выход через прокси
неразличимы (1 из 8 в обоих условиях), то есть дело не в IP.

Свежего КОНТЕКСТА не хватает: в контрольном замере каждая карточка бралась
через browser.new_page(), в новом изолированном контексте, — и всё равно отказ
со второй. Признак живёт на уровне процесса, camoufox генерирует отпечаток при
запуске, а не при создании контекста. Отсюда: reset_context (#3118) эту задачу
не решает в принципе. Цена перезапуска — 0.6 с (3.5 с только первый, холодный).

- PROVIDERS: добавлен "domclick" (+ host-detect). Раньше он проваливался в
  generic и делил браузер со счётчиком страниц с прочим трафиком — при пороге
  перезапуска 1 это было бы неверно.
- BROWSER_RECYCLE_PAGES стал поставщик-зависимым (_resolve_recycle_pages +
  BROWSER_RECYCLE_PAGES_{PROVIDER}), по образцу BROWSER_BLOCK_IMAGES_{PROVIDER}
  из #3185. Код-дефолт domclick=1, остальным прежние 15 — у Авито и Циана узор
  другой и своего замера под него нет.

Отдельно починены ~24 холостых охранника в тестах. Они делали
monkeypatch.setattr(server, "BROWSER_RECYCLE_PAGES", 10_000), чтобы запретить
перезапуск браузера; после перехода на словарь этот патч перестал на что-либо
влиять, и набор оставался зелёным лишь потому, что ни один тест не делает 15
страниц подряд. Теперь патчится _RECYCLE_PAGES_BY_PROVIDER, а сама глобальная
константа убрана, чтобы её не патчили снова. Проверено мутацией: при пороге 1
для всех провайдеров падают ровно три теста, которые этот дефолт и проверяют,
остальные 155 удерживаются — значит охранники работают.
lekss361 merged commit c09215f779 into main 2026-08-29 10:16:56 +00:00
lekss361 deleted branch fix/3205-domclick-recycle 2026-08-29 10:16:56 +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#3206
No description provided.