-- 228_scrape_proxies_browser_health.sql -- Здоровье узла ОТДЕЛЬНО для браузерного тракта (#2723). -- -- WHY: -- `run_proxy_healthcheck` гоняет через узел обычный httpx-GET к ipify. Боевой сбор -- Авито с 02.08 (#2637) ходит через сайдкар браузером: camoufox стартует С ЭТИМ -- прокси (geoip-lookup на launch), потом навигация. Это разные свойства узла: -- крошечный GET проходит там, где launch/навигация падает (`browser unavailable -- (proxy may be down)` — все 90 записанных обрывов сбора именно такие). -- -- Хуже того, оба свойства писались в ОДИН счётчик: боевой /fetch репортит -- mark_health(ok=False) → consecutive_fails++, но следующая (≤30 мин) успешная -- ipify-проба делает consecutive_fails=0 + enabled=true. Дешёвая проба СТИРАЛА -- вердикт дорогого тракта, и узел, мёртвый для браузера, вечно возвращался в -- выдачу. Это ровно ошибка #2686 (схлопывание двух диагнозов в один флаг) в -- другом месте; разводим её тем же приёмом, что #2711 (`scrape_runs.ban_kind`) — -- поле РЯДОМ, а не новое значение существующего флага. -- -- WHAT (три колонки, ни одна не участвует в enabled/consecutive_fails): -- browser_fail_streak — подряд-провалы ИМЕННО браузерной пробы, и только те, что -- атрибутируются узлу (сайдкар лежит / страница пустая — -- не считаются, см. proxy_pool._classify_browser_probe). -- Успешная ipify-проба его НЕ обнуляет — в этом весь смысл. -- browser_unfit_since — момент, когда streak дошёл до порога. NOT NULL = «жив для -- HTTP, непригоден для браузера». acquire() такой узел НЕ -- исключает (голодание хуже — #2600/#2638, пул 4 узла), а -- отправляет в КОНЕЦ очереди выдачи: его возьмут, только -- если свободных пригодных нет. -- browser_check_at — когда браузерную пробу гоняли последний раз. Такт у неё -- свой, редкий (BROWSER_PROBE_MINUTES): она стоит запуска -- camoufox (~8с замерено на проде), ipify — миллисекунды. -- -- IDEMPOTENCY / SAFETY: -- ADD COLUMN IF NOT EXISTS × 3, аддитивно, без backfill'а: NULL/0 = «браузерную -- пробу ещё не гоняли», ровно то состояние, в котором пул и находится. Ни одна -- существующая выборка не меняет результат (все три колонки новые). Повторный -- прогон — no-op (auto-apply strict на деплое это требует). -- -- Dependencies: 157_scrape_proxies.sql BEGIN; ALTER TABLE scrape_proxies ADD COLUMN IF NOT EXISTS browser_fail_streak integer NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS browser_unfit_since timestamptz, ADD COLUMN IF NOT EXISTS browser_check_at timestamptz; COMMENT ON COLUMN scrape_proxies.browser_fail_streak IS 'Подряд-провалы браузерной пробы (сайдкар + camoufox через ЭТОТ узел), ' 'атрибутированные узлу. НЕ обнуляется успешной ipify-пробой — иначе дешёвая ' 'проба стирает вердикт дорогого тракта (#2723). Обнуляется успешной браузерной ' 'пробой. Порог → browser_unfit_since, см. proxy_pool.BROWSER_UNFIT_THRESHOLD.'; COMMENT ON COLUMN scrape_proxies.browser_unfit_since IS 'NOT NULL = узел жив для обычного HTTP, но браузерный тракт через него не ' 'работает (#2723). Это НЕ enabled=false: узел остаётся в пуле и обслуживает ' 'curl-путь, а acquire() лишь отдаёт его последним. Полное выключение по-прежнему ' 'значит «узел мёртв целиком» (серия транспортных сбоев) либо решение оператора.'; COMMENT ON COLUMN scrape_proxies.browser_check_at IS 'Последняя браузерная проба. Такт свой, редкий (proxy_pool.BROWSER_PROBE_MINUTES): ' 'одна такая проба = запуск camoufox (~8с на проде), против миллисекунд у ipify. ' 'На неподтверждённом провале НЕ обновляется — чтобы следующий же цикл ' 'healthcheck подтвердил/опроверг отказ, а не ждал полный такт.'; COMMIT;