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
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
Почему: `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
This commit is contained in:
parent
9f9086fa4d
commit
15ca70ad70
1 changed files with 71 additions and 29 deletions
|
|
@ -28,7 +28,8 @@ jobs:
|
||||||
frontend: ${{ steps.set-all.outputs.frontend || steps.filter.outputs.frontend }}
|
frontend: ${{ steps.set-all.outputs.frontend || steps.filter.outputs.frontend }}
|
||||||
browser: ${{ steps.set-all.outputs.browser || steps.filter.outputs.browser }}
|
browser: ${{ steps.set-all.outputs.browser || steps.filter.outputs.browser }}
|
||||||
infra: ${{ steps.set-all.outputs.infra || steps.filter.outputs.infra }}
|
infra: ${{ steps.set-all.outputs.infra || steps.filter.outputs.infra }}
|
||||||
scraper: ${{ steps.set-all.outputs.scraper || steps.filter.outputs.scraper }}
|
# Отдельного `scraper`-признака больше нет (#2679) — см. SCRAPER_RECREATE
|
||||||
|
# в job deploy: scraper/tgbot бегут ТОТ ЖЕ образ, что и backend.
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
|
@ -91,7 +92,6 @@ jobs:
|
||||||
echo "frontend=true" >> "$GITHUB_OUTPUT"
|
echo "frontend=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "browser=true" >> "$GITHUB_OUTPUT"
|
echo "browser=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "infra=true" >> "$GITHUB_OUTPUT"
|
echo "infra=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "scraper=true" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# Cumulative diff: compare deployed SHA → HEAD so that a fast chain of merges
|
# Cumulative diff: compare deployed SHA → HEAD so that a fast chain of merges
|
||||||
# (e.g. backend #1829 then frontend #1830) doesn't lose earlier changes.
|
# (e.g. backend #1829 then frontend #1830) doesn't lose earlier changes.
|
||||||
|
|
@ -115,20 +115,20 @@ jobs:
|
||||||
- 'tradein-mvp/docker-compose.prod.yml'
|
- 'tradein-mvp/docker-compose.prod.yml'
|
||||||
- 'tradein-mvp/deploy/**'
|
- 'tradein-mvp/deploy/**'
|
||||||
- '.forgejo/workflows/deploy-tradein.yml'
|
- '.forgejo/workflows/deploy-tradein.yml'
|
||||||
scraper:
|
# УДАЛЁН фильтр `scraper` (#2679, 2026-08-05). Он был allowlist'ом
|
||||||
- 'tradein-mvp/backend/app/services/scrapers/**'
|
# «файлов, которые исполняет планировщик», и перечислял только то,
|
||||||
- 'tradein-mvp/backend/app/services/scrape_pipeline.py'
|
# что вспомнили. Дважды выстрелило одинаково:
|
||||||
- 'tradein-mvp/backend/app/services/scheduler.py'
|
# 2026-07-02 (#2188) — fias-dedup доехал до tradein-backend, но не
|
||||||
- 'tradein-mvp/backend/app/scheduler_main.py'
|
# до tradein-scraper; починили ДОБАВЛЕНИЕМ путей (matching/**,
|
||||||
- 'tradein-mvp/backend/app/tasks/**'
|
# house_dedup_merge.py) — залатали случай, не механизм;
|
||||||
# #2188: scheduler исполняет matching/dedup при каждом scrape-тике —
|
# 2026-08-05 (#2675) — house_imv_backfill.py + product_handlers.py
|
||||||
# без этих путей scraper-контейнер оставался на старом коде
|
# в списке не значились → планировщик час крутил старый код,
|
||||||
# (2026-07-02: fias-dedup доехал до tradein-backend, но не до
|
# деплой при этом отчитался успехом.
|
||||||
# tradein-scraper). После USE_KIT_SCHEDULER=true kit-код и есть
|
# За июнь-август 48% (193 из 402) backend-мержей не попадали ни в
|
||||||
# scheduler — его правки тоже обязаны пересоздавать контейнер.
|
# один из путей списка, т.е. половина правок доезжала до scraper'а
|
||||||
- 'tradein-mvp/backend/app/services/matching/**'
|
# только со следующим «удачным» деплоем. Теперь пересоздание
|
||||||
- 'tradein-mvp/backend/app/services/house_dedup_merge.py'
|
# привязано не к списку файлов, а к факту пересборки образа —
|
||||||
- 'tradein-mvp/packages/scraper-kit/**'
|
# см. SCRAPER_RECREATE в job deploy.
|
||||||
|
|
||||||
# Quality gate: pytest MUST pass before any image is built/deployed (#666).
|
# Quality gate: pytest MUST pass before any image is built/deployed (#666).
|
||||||
# Runs the tradein-mvp/backend suite; a red test blocks build + deploy.
|
# Runs the tradein-mvp/backend suite; a red test blocks build + deploy.
|
||||||
|
|
@ -295,20 +295,28 @@ jobs:
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: latest
|
IMAGE_TAG: latest
|
||||||
GHCR_PAT: ${{ secrets.GHCR_PAT }}
|
GHCR_PAT: ${{ secrets.GHCR_PAT }}
|
||||||
# Phase 0: generic infra edits (compose / workflow / deploy/**) must NOT
|
# #2679: backend / scraper / tgbot — ОДИН И ТОТ ЖЕ образ
|
||||||
# recreate the scraper and SIGKILL a running multi-hour job. Only genuine
|
# gendesign-tradein-backend (см. docker-compose.prod.yml: три сервиса,
|
||||||
# scraper-code paths (the `scraper` paths-filter already covers
|
# одна строка image, разный command). Значит вопрос «пересоздавать ли
|
||||||
# app/services/scrapers/**, scrape_pipeline.py, scheduler.py,
|
# scraper» — это не «трогали ли его файлы», а «мог ли пересобраться
|
||||||
# scheduler_main.py, app/tasks/**) — or a manual workflow_dispatch —
|
# образ». Условие ОБЯЗАНО совпадать с `if:` джобы build-backend:
|
||||||
# should trigger a scraper recreate. (infra term intentionally dropped.)
|
# backend || infra || workflow_dispatch. Ровно тогда в реестре мог
|
||||||
SCRAPER_CHANGED: ${{ needs.changes.outputs.scraper == 'true' || github.event_name == 'workflow_dispatch' }}
|
# появиться новый :latest, и оставить scraper на старом — значит
|
||||||
|
# оставить планировщик на старом коде (инцидент #2679).
|
||||||
|
#
|
||||||
|
# Раньше здесь стоял «Phase 0»-компромисс: infra-правки намеренно НЕ
|
||||||
|
# пересоздавали scraper, чтобы не убить многочасовой прогон. Компромисс
|
||||||
|
# больше не нужен — с #1951 перед recreate'ом идёт graceful drain
|
||||||
|
# (ждём scrape_runs до 5 мин) + startup-reap осиротевших строк, а сам
|
||||||
|
# `compose up -d` на неизменившемся образе — no-op.
|
||||||
|
SCRAPER_RECREATE: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.infra == 'true' || github.event_name == 'workflow_dispatch' }}
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
with:
|
with:
|
||||||
host: ${{ secrets.DEPLOY_HOST }}
|
host: ${{ secrets.DEPLOY_HOST }}
|
||||||
username: ${{ secrets.DEPLOY_USER }}
|
username: ${{ secrets.DEPLOY_USER }}
|
||||||
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
key: ${{ secrets.DEPLOY_SSH_KEY }}
|
||||||
port: ${{ secrets.DEPLOY_PORT }}
|
port: ${{ secrets.DEPLOY_PORT }}
|
||||||
envs: IMAGE_TAG,GHCR_PAT,SCRAPER_CHANGED,GITHUB_SHA
|
envs: IMAGE_TAG,GHCR_PAT,SCRAPER_RECREATE,GITHUB_SHA
|
||||||
script: |
|
script: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd /opt/gendesign
|
cd /opt/gendesign
|
||||||
|
|
@ -489,8 +497,9 @@ jobs:
|
||||||
# с browser/backend/frontend, отдельного graceful-drain не требует.
|
# с browser/backend/frontend, отдельного graceful-drain не требует.
|
||||||
SERVICES="browser backend frontend tgbot"
|
SERVICES="browser backend frontend tgbot"
|
||||||
SCRAPER_STOP_TS=""
|
SCRAPER_STOP_TS=""
|
||||||
if [ "${SCRAPER_CHANGED:-true}" = "true" ]; then
|
if [ "${SCRAPER_RECREATE:-true}" = "true" ]; then
|
||||||
echo "→ scraper paths changed — waiting for in-flight scrape_runs to drain (up to 5 min)"
|
echo "→ backend-образ мог пересобраться — scraper пересоздаётся вместе с backend (#2679);"
|
||||||
|
echo " ждём слива in-flight scrape_runs (до 5 мин)"
|
||||||
drained=""
|
drained=""
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
# NB: не сливать "psql не ответил" с "0 running" — иначе неудачный
|
# NB: не сливать "psql не ответил" с "0 running" — иначе неудачный
|
||||||
|
|
@ -530,12 +539,13 @@ jobs:
|
||||||
|
|
||||||
SERVICES="$SERVICES scraper"
|
SERVICES="$SERVICES scraper"
|
||||||
else
|
else
|
||||||
echo "→ scraper unchanged — tradein-scraper left running (подхватит новый image при следующем своём рестарте)"
|
echo "→ backend-образ в этом деплое не пересобирался — tradein-scraper не трогаем"
|
||||||
|
echo " (сверка образов ниже всё равно проверит, что он не отстал)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps $SERVICES
|
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps $SERVICES
|
||||||
|
|
||||||
if [ "${SCRAPER_CHANGED:-true}" = "true" ] && [ -n "${SCRAPER_STOP_TS:-}" ]; then
|
if [ "${SCRAPER_RECREATE:-true}" = "true" ] && [ -n "${SCRAPER_STOP_TS:-}" ]; then
|
||||||
echo "→ Startup-reap (#1951): помечаем orphaned running-строки, замороженные recreate'ом"
|
echo "→ Startup-reap (#1951): помечаем orphaned running-строки, замороженные recreate'ом"
|
||||||
# NB: psql `-c` НЕ поддерживает `:'var'`-подстановку (переменная доходит до
|
# NB: psql `-c` НЕ поддерживает `:'var'`-подстановку (переменная доходит до
|
||||||
# сервера как литерал → syntax error, см. комментарий выше про TRADEIN_READER_PASSWORD)
|
# сервера как литерал → syntax error, см. комментарий выше про TRADEIN_READER_PASSWORD)
|
||||||
|
|
@ -653,7 +663,7 @@ jobs:
|
||||||
# снижает шанс поймать контейнер ровно в момент between-restarts
|
# снижает шанс поймать контейнер ровно в момент between-restarts
|
||||||
# промежуточного "running" внутри crash-loop.
|
# промежуточного "running" внутри crash-loop.
|
||||||
# tgbot пересоздаётся на КАЖДОМ деплое (безусловно в $SERVICES);
|
# tgbot пересоздаётся на КАЖДОМ деплое (безусловно в $SERVICES);
|
||||||
# scraper — только когда SCRAPER_CHANGED (см. блок выше) — поэтому
|
# scraper — только когда SCRAPER_RECREATE (см. блок выше) — поэтому
|
||||||
# проверяем только то, что реально входит в текущий $SERVICES.
|
# проверяем только то, что реально входит в текущий $SERVICES.
|
||||||
for svc in tgbot scraper; do
|
for svc in tgbot scraper; do
|
||||||
case " $SERVICES " in
|
case " $SERVICES " in
|
||||||
|
|
@ -683,6 +693,38 @@ jobs:
|
||||||
echo "→ tradein-$svc running."
|
echo "→ tradein-$svc running."
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Сверка образов backend-семейства (#2679) — последняя проверка перед
|
||||||
|
# маркером «задеплоено». backend/scraper/tgbot бегут ОДИН образ
|
||||||
|
# gendesign-tradein-backend; backend пересоздаётся на каждом деплое
|
||||||
|
# (безусловно в $SERVICES) и потому всегда несёт свежий :latest —
|
||||||
|
# он и есть эталон. Если у scraper или tgbot image ID другой, значит
|
||||||
|
# контейнер остался на старом коде, а деплой без этой проверки
|
||||||
|
# отчитался бы успехом: ровно инцидент 2026-08-05 (#2675 доехал до
|
||||||
|
# tradein-backend, ff98603ba3cc; tradein-scraper остался на
|
||||||
|
# da26154c64a6 часовой давности — а планировщик, единственный
|
||||||
|
# исполнитель домовой оценки, живёт именно там).
|
||||||
|
# Падаем, а не warning'уем: расхождение = правка не работает, и
|
||||||
|
# узнать об этом надо в момент деплоя, а не через месяц. exit 1 идёт
|
||||||
|
# ДО записи .tradein-deployed-sha → следующий прогон возьмёт ту же
|
||||||
|
# базу и пересоберёт всё накопленное (тот же приём, что в health-check).
|
||||||
|
backend_image=$(docker inspect -f '{{.Image}}' tradein-backend 2>/dev/null || echo "missing")
|
||||||
|
image_mismatch=""
|
||||||
|
for svc in scraper tgbot; do
|
||||||
|
svc_image=$(docker inspect -f '{{.Image}}' "tradein-$svc" 2>/dev/null || echo "missing")
|
||||||
|
if [ "$svc_image" != "$backend_image" ]; then
|
||||||
|
echo "ERROR: tradein-$svc на образе $svc_image, tradein-backend — на $backend_image"
|
||||||
|
image_mismatch="yes"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$image_mismatch" ]; then
|
||||||
|
echo "ERROR: образы backend-семейства разошлись — деплой FAILED (#2679)."
|
||||||
|
echo " Лечение вручную: docker compose -p gendesign-tradein \\"
|
||||||
|
echo " -f /opt/gendesign/tradein-mvp/docker-compose.prod.yml \\"
|
||||||
|
echo " up -d --force-recreate --no-deps scraper tgbot"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "→ образы совпадают: backend/scraper/tgbot на $backend_image."
|
||||||
|
|
||||||
# Cleanup старых образов
|
# Cleanup старых образов
|
||||||
for repo in ghcr.io/lekss361/gendesign-tradein-backend \
|
for repo in ghcr.io/lekss361/gendesign-tradein-backend \
|
||||||
ghcr.io/lekss361/gendesign-tradein-frontend; do
|
ghcr.io/lekss361/gendesign-tradein-frontend; do
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue