fix(tradein/deploy): скрапер пересобирается вместе с бэкендом + сверка образов после деплоя (#2679) #2680

Merged
lekss361 merged 2 commits from fix/2679-scraper-deploy-parity into main 2026-08-06 17:53:32 +00:00
Collaborator

Что было

tradein-backend, tradein-scraper, tradein-tgbotодин образ
gendesign-tradein-backenddocker-compose.prod.yml три сервиса с одной строкой
image: и разными command). Но пересоздание скрапера было привязано к отдельному
paths-фильтру scraper — allowlist'у «файлов, которые исполняет планировщик».

Список перечислял только то, что вспомнили, и выстрелил дважды одинаково:

  • 2026-07-02 (#2188) — fias-dedup доехал до tradein-backend, но не до
    tradein-scraper; починили добавлением путей (matching/**, house_dedup_merge.py);
  • 2026-08-05 (#2675)house_imv_backfill.py и product_handlers.py в списке не
    значились; планировщик (SCHEDULER_ENABLE=true живёт именно в скрапере) час крутил
    старый код, деплой при этом отчитался успехом.

Масштаб дыры: за июнь–август 48% (193 из 402) мержей, затрагивающих backend-образ,
не попадали ни в один путь списка — то есть половина правок доезжала до скрапера только
со следующим «удачным» деплоем.

Что стало

  1. Фильтр scraper удалён вместе с выходом changes.outputs.scraper. Признак
    пересоздания (SCRAPER_RECREATE) теперь буквально повторяет if: джобы
    build-backend: backend || infra || workflow_dispatch — «в реестре мог появиться
    новый :latest».
  2. Drain — только когда есть что пересоздавать. Сразу после docker compose pull
    сравнивается ID подтянутого образа с тем, на котором реально бежит tradein-scraper.
    Совпало → «пересоздавать нечего», пятиминутное ожидание слива scrape_runs
    пропускается, in-flight сбор не трогаем. Не совпало (или контейнера/тега нет) →
    drain как раньше. Порядок обязателен: до pull под тегом :latest лежит ещё старый
    образ, и сравнение «совпало бы» ровно тогда, когда ждать как раз нужно.
  3. Сверка образов после деплоя. После health-check'ов сравниваются image ID
    tradein-backend (эталон — он пересоздаётся на каждом деплое) и
    tradein-scraper / tradein-tgbot. Расхождение → exit 1 до записи
    .tradein-deployed-sha, с командой ручного лечения в логе. «Контейнер отстал» и
    «контейнера нет» — разные сообщения: это разные аварии.

Побочно закрыт ложный startup-reap: чекпоинт и reap завязаны на тот же признак «образ
действительно новый». Без этого прогон, переживший таймаут drain'а в не пересозданном
контейнере, помечался бы cancelled, продолжая работать.

Снят «Phase 0»-компромисс (infra-правки намеренно не трогали скрапер, чтобы не убить
многочасовой прогон): с #1951 есть graceful drain + startup-reap, а теперь ещё и
пропуск ожидания, когда образ не поменялся.

Почему именно так, а не «дописать пути»

Дописанный путь чинит один случай; механизм остаётся угадыванием и ломается на следующем
новом файле — это уже воспроизвелось через месяц. Здесь признак сведён к тому, от чего
он физически зависит: собран новый образ → пересоздаём все контейнеры этого образа.
Список файлов больше не нужно поддерживать, забыть его нечем.

Сверка образов оставлена дополнительно к этому, а не вместо: она ловит расхождение
любого происхождения (ручной docker на проде, неудавшийся recreate, будущая правка
пайплайна) и делает дыру громкой в момент деплоя.

Цена по времени

43 прод-деплоя tradein за 31.07–05.08 (Forgejo API, длительность job deploy):

деплой median mean max
backend-правка → образ новый, drain (38 шт.) 86s 110s 477s
frontend/browser-only, скрапер не трогаем (5 шт.) 56s 59s 69s
справочно: попадал в старый allowlist (18 из 43) 113s 146s 477s

Что меняется по факту: платят только деплои с реально новым образом — те 48%
backend-мержей, что раньше молча проходили мимо скрапера, теперь пересоздают его и
получают drain (median +37s, mean +72s, в худшем случае +5 мин — потолок ожидания при
длинном full-load'е). Скрап-прогоны занимают 37% wall-clock времени (14 дней, 1132
прогона, средний остров занятости 11 мин, максимальный 11 ч) — отсюда разброс.

Infra-правки (compose / workflow / deploy/**) не платят ничего: образ собирается по
кэшу тем же, сравнение совпадает, drain пропускается. Таких мержей с июня было 7 из 499
— редкий, но это ровно класс PR'ов вроде текущего, и раньше каждый из них рисковал
прервать многочасовой сбор ради no-op up -d.

Логика пропуска джоб не затронута: test / build-* / deploy зависят от
backend / infra / frontend / browser, scraper-выход нигде больше не использовался.

Что будет при следующем деплое

Этот PR трогает сам deploy-tradein.ymlinfra=truebuild-backend +
SCRAPER_RECREATE=true. Сравнение образов покажет расхождение (сейчас на проде
tradein-backend = ff98603ba3cc, tradein-scraper = da26154c64a6 часовой давности),
значит drain отработает и скрапер пересоздастся — код #2675 наконец доедет до
планировщика
. Сверка образов после этого пройдёт зелёной и с этого момента охраняет
инвариант.

Если бы расхождение осталось, деплой упал бы на новой проверке — и это чинится само:
маркер SHA не записан → следующий прогон возьмёт ту же базу, пересоберёт накопленное и
пересоздаст скрапер.

Проверка

  • yaml.safe_load на файле; bash -n на теле deploy-скрипта.
  • Оба новых блока прогнаны локально на подменённом docker:
    сверка — совпали → exit 0, отстал (реальные ID инцидента) → exit 1 с указанием
    контейнера, контейнера нет → exit 1 с другим сообщением, нет tradein-backend
    exit 1; решение о drain — образ тот же → пропуск, новый → drain, контейнера нет →
    drain, тег не разрешился → drain (fail-safe в сторону ожидания).
  • Прод проверен только на чтение (docker ps, docker inspect, SELECT из
    scrape_runs). Ничего не перезапускалось.
  • После merge — убедиться, что деплой зелёный и tradein-scraper встал на тот же
    image ID, что tradein-backend.

Известный потолок

Правка compose, меняющая только конфиг сервиса scraper (env, лимиты) при том же образе,
пересоздаст контейнер без drain'а: сравниваются образы, а не конфиг. Страхуют
SIGTERM-drain (#1182) + stop_grace_period 120s, строку прогона подчистит периодический
6h zombie-reaper. Вариант «спросить у compose через up --dry-run, будет ли recreate»
сознательно не делал — парсинг его вывода хрупче, чем цена этого случая.

Не входит в PR

  • deploy.yml (ПТИЦА) не тронут — там своя история (#2462). На момент правки
    расхождения в том стеке нет: gendesign-backend-1 и gendesign-beat-1 на одном
    image ID, worker собирается из отдельного образа.
  • Пересоздание контейнеров на проде вручную — решение владельца.

Refs #2679, #2675, #2188

## Что было `tradein-backend`, `tradein-scraper`, `tradein-tgbot` — **один образ** `gendesign-tradein-backend` (в `docker-compose.prod.yml` три сервиса с одной строкой `image:` и разными `command`). Но пересоздание скрапера было привязано к отдельному paths-фильтру `scraper` — allowlist'у «файлов, которые исполняет планировщик». Список перечислял только то, что вспомнили, и выстрелил дважды одинаково: - **2026-07-02 (#2188)** — fias-dedup доехал до `tradein-backend`, но не до `tradein-scraper`; починили *добавлением* путей (`matching/**`, `house_dedup_merge.py`); - **2026-08-05 (#2675)** — `house_imv_backfill.py` и `product_handlers.py` в списке не значились; планировщик (`SCHEDULER_ENABLE=true` живёт именно в скрапере) час крутил старый код, деплой при этом отчитался успехом. Масштаб дыры: за июнь–август **48% (193 из 402)** мержей, затрагивающих backend-образ, не попадали ни в один путь списка — то есть половина правок доезжала до скрапера только со следующим «удачным» деплоем. ## Что стало 1. **Фильтр `scraper` удалён** вместе с выходом `changes.outputs.scraper`. Признак пересоздания (`SCRAPER_RECREATE`) теперь буквально повторяет `if:` джобы `build-backend`: `backend || infra || workflow_dispatch` — «в реестре мог появиться новый `:latest`». 2. **Drain — только когда есть что пересоздавать.** Сразу после `docker compose pull` сравнивается ID подтянутого образа с тем, на котором реально бежит `tradein-scraper`. Совпало → «пересоздавать нечего», пятиминутное ожидание слива `scrape_runs` пропускается, in-flight сбор не трогаем. Не совпало (или контейнера/тега нет) → drain как раньше. Порядок обязателен: до `pull` под тегом `:latest` лежит ещё старый образ, и сравнение «совпало бы» ровно тогда, когда ждать как раз нужно. 3. **Сверка образов после деплоя.** После health-check'ов сравниваются image ID `tradein-backend` (эталон — он пересоздаётся на каждом деплое) и `tradein-scraper` / `tradein-tgbot`. Расхождение → `exit 1` **до** записи `.tradein-deployed-sha`, с командой ручного лечения в логе. «Контейнер отстал» и «контейнера нет» — разные сообщения: это разные аварии. Побочно закрыт ложный startup-reap: чекпоинт и reap завязаны на тот же признак «образ действительно новый». Без этого прогон, переживший таймаут drain'а в **не** пересозданном контейнере, помечался бы `cancelled`, продолжая работать. Снят «Phase 0»-компромисс (infra-правки намеренно не трогали скрапер, чтобы не убить многочасовой прогон): с #1951 есть graceful drain + startup-reap, а теперь ещё и пропуск ожидания, когда образ не поменялся. ## Почему именно так, а не «дописать пути» Дописанный путь чинит один случай; механизм остаётся угадыванием и ломается на следующем новом файле — это уже воспроизвелось через месяц. Здесь признак сведён к тому, от чего он физически зависит: собран новый образ → пересоздаём все контейнеры этого образа. Список файлов больше не нужно поддерживать, забыть его нечем. Сверка образов оставлена **дополнительно** к этому, а не вместо: она ловит расхождение любого происхождения (ручной `docker` на проде, неудавшийся recreate, будущая правка пайплайна) и делает дыру громкой в момент деплоя. ## Цена по времени 43 прод-деплоя tradein за 31.07–05.08 (Forgejo API, длительность job `deploy`): | деплой | median | mean | max | |---|---|---|---| | backend-правка → образ новый, drain (38 шт.) | 86s | 110s | 477s | | frontend/browser-only, скрапер не трогаем (5 шт.) | 56s | 59s | 69s | | *справочно:* попадал в старый allowlist (18 из 43) | 113s | 146s | 477s | Что меняется по факту: **платят только деплои с реально новым образом** — те 48% backend-мержей, что раньше молча проходили мимо скрапера, теперь пересоздают его и получают drain (median +37s, mean +72s, в худшем случае +5 мин — потолок ожидания при длинном full-load'е). Скрап-прогоны занимают 37% wall-clock времени (14 дней, 1132 прогона, средний остров занятости 11 мин, максимальный 11 ч) — отсюда разброс. Infra-правки (compose / workflow / `deploy/**`) не платят ничего: образ собирается по кэшу тем же, сравнение совпадает, drain пропускается. Таких мержей с июня было 7 из 499 — редкий, но это ровно класс PR'ов вроде текущего, и раньше каждый из них рисковал прервать многочасовой сбор ради no-op `up -d`. Логика пропуска джоб не затронута: `test` / `build-*` / `deploy` зависят от `backend` / `infra` / `frontend` / `browser`, `scraper`-выход нигде больше не использовался. ## Что будет при следующем деплое Этот PR трогает сам `deploy-tradein.yml` → `infra=true` → `build-backend` + `SCRAPER_RECREATE=true`. Сравнение образов покажет расхождение (сейчас на проде `tradein-backend` = `ff98603ba3cc`, `tradein-scraper` = `da26154c64a6` часовой давности), значит drain отработает и скрапер пересоздастся — **код #2675 наконец доедет до планировщика**. Сверка образов после этого пройдёт зелёной и с этого момента охраняет инвариант. Если бы расхождение осталось, деплой упал бы на новой проверке — и это чинится само: маркер SHA не записан → следующий прогон возьмёт ту же базу, пересоберёт накопленное и пересоздаст скрапер. ## Проверка - [x] `yaml.safe_load` на файле; `bash -n` на теле deploy-скрипта. - [x] Оба новых блока прогнаны локально на подменённом `docker`: сверка — совпали → exit 0, отстал (реальные ID инцидента) → exit 1 с указанием контейнера, контейнера нет → exit 1 с другим сообщением, нет `tradein-backend` → exit 1; решение о drain — образ тот же → пропуск, новый → drain, контейнера нет → drain, тег не разрешился → drain (fail-safe в сторону ожидания). - [x] Прод проверен только на чтение (`docker ps`, `docker inspect`, SELECT из `scrape_runs`). Ничего не перезапускалось. - [ ] После merge — убедиться, что деплой зелёный и `tradein-scraper` встал на тот же image ID, что `tradein-backend`. ## Известный потолок Правка compose, меняющая только конфиг сервиса `scraper` (env, лимиты) при том же образе, пересоздаст контейнер **без** drain'а: сравниваются образы, а не конфиг. Страхуют SIGTERM-drain (#1182) + `stop_grace_period` 120s, строку прогона подчистит периодический 6h zombie-reaper. Вариант «спросить у compose через `up --dry-run`, будет ли recreate» сознательно не делал — парсинг его вывода хрупче, чем цена этого случая. ## Не входит в PR - `deploy.yml` (ПТИЦА) не тронут — там своя история (#2462). На момент правки расхождения в том стеке нет: `gendesign-backend-1` и `gendesign-beat-1` на одном image ID, `worker` собирается из отдельного образа. - Пересоздание контейнеров на проде вручную — решение владельца. Refs #2679, #2675, #2188
bot-backend added 1 commit 2026-08-05 20:50:53 +00:00
fix(tradein/deploy): скрапер пересобирается вместе с бэкендом + сверка образов после деплоя (#2679)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 6s
CI / 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
15ca70ad70
Почему: `tradein-backend`, `tradein-scraper` и `tradein-tgbot` — один образ
gendesign-tradein-backend, но пересоздание скрапера было привязано к allowlist'у
путей («файлы, которые исполняет планировщик»). Список перечислял только то, что
вспомнили: 2026-07-02 (#2188) на нём погорел fias-dedup — починили ДОБАВЛЕНИЕМ
путей; 2026-08-05 (#2675) тот же механизм выстрелил снова на
house_imv_backfill.py / product_handlers.py. За июнь-август 48% (193 из 402)
backend-мержей не попадали ни в один путь списка.

Что сделано вместо очередного пополнения списка:
- фильтр `scraper` удалён; признак пересоздания = `if:` джобы build-backend
  (backend || infra || workflow_dispatch), т.е. «образ мог пересобраться»;
- добавлена сверка image ID backend/scraper/tgbot после health-checks: при
  расхождении деплой падает ДО записи .tradein-deployed-sha, а не отчитывается
  успехом. Следующий прогон возьмёт ту же базу и пересоберёт накопленное.

«Phase 0»-компромисс (infra не трогает скрапер, чтобы не убить многочасовой
прогон) снят: с #1951 перед recreate'ом идёт graceful drain + startup-reap,
а `compose up -d` на неизменившемся образе — no-op.

Цена по факту (43 прод-деплоя 31.07-05.08): деплой со скрапером median 113s /
mean 146s против 76s / 74s без него; средний деплой вырастет примерно на 40s,
худший случай — до +5 мин (потолок drain'а при длинном full-load'е).

Refs #2679
Light1YT added 1 commit 2026-08-05 20:58:38 +00:00
fix(tradein/deploy): не уходить в drain, когда пересоздавать нечего (#2679)
All checks were successful
CI Trade-In / changes (pull_request) Successful in 7s
CI / changes (pull_request) Successful in 8s
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
bb6e5c7e41
`SCRAPER_RECREATE` истинно и на infra-правках (compose / workflow / deploy/**),
а те почти всегда собирают тот же образ по кэшу: digest не меняется, `up -d`
выходит no-op — и за него платили пятиминутным ожиданием слива scrape_runs,
прерывая сбор. Теперь после `docker compose pull` (порядок важен: до pull'а под
:latest ещё старый образ) сравниваем ID подтянутого образа с тем, на котором
бежит tradein-scraper. Совпало — печатаем «пересоздавать нечего» и идём дальше
без drain'а; не совпало или контейнера/тега нет — drain как раньше.

Заодно закрыт ложный startup-reap: чекпоинт и reap завязаны на тот же признак и
больше не выполняются, когда recreate'а не было. Иначе прогон, переживший
таймаут drain'а в НЕ пересозданном контейнере, помечался бы 'cancelled',
продолжая работать.

scraper остаётся в $SERVICES в обоих случаях — при совпавшем образе `up -d`
no-op, но правка самого compose (env/лимиты сервиса) так всё же доезжает.

Сверка образов: «контейнера нет» и «контейнер отстал» теперь разные сообщения —
это разные аварии и чинятся по-разному; отсутствие tradein-backend (эталона)
тоже отдельная строка.

Refs #2679
lekss361 merged commit d3d7464267 into main 2026-08-06 17:53: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#2680
No description provided.