fix(tradein/deploy): atomic recreate + graceful scraper drain + startup-reap (#1951) #2388
2 changed files with 97 additions and 6 deletions
|
|
@ -451,17 +451,103 @@ jobs:
|
|||
fi
|
||||
|
||||
# (4) Теперь — новый app-код: схема уже актуальна.
|
||||
# Селективный up: scraper НЕ пересоздаём на каждый backend-деплой (#1182) —
|
||||
# один image :latest на backend+scraper, блочный up -d рестартовал бы scraper
|
||||
# и убивал бегущий sweep. Scraper поднимается только при scraper/infra изменениях.
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps browser backend frontend
|
||||
#
|
||||
# #1951: раньше scraper пересоздавался ВТОРОЙ отдельной командой `up -d`,
|
||||
# уже ПОСЛЕ browser/backend/frontend. Если это происходило посреди
|
||||
# in-flight sweep'а (avito/cian/rosreestr full-load), процесс убивался на
|
||||
# лету, а осиротевшая scrape_runs-строка сидела status='running' с
|
||||
# замёрзшим heartbeat до периодического 6h zombie-reaper'а — false "hang"
|
||||
# investigation вместо честного deploy-артефакта (см. cian_full_load #404).
|
||||
# Три меры ниже — все devops-only (shell в deploy-скрипте), БЕЗ правок
|
||||
# Python в scraper-стартап-пути (scheduler_main.py / scraper-kit /
|
||||
# app/services/scrapers — намеренно не тронуты, см. PR-описание):
|
||||
#
|
||||
# 1) Атомарный recreate — browser/backend/frontend[+scraper] поднимаются
|
||||
# ОДНИМ `docker compose up -d` инвокейшном (список сервисов собирается
|
||||
# заранее в $SERVICES), а не двумя последовательными командами.
|
||||
# 2) Graceful drain — если scraper будет пересоздан, ждём (до 5 мин, poll
|
||||
# каждые 10s) пока scrape_runs.status='running' не станет 0, ПРЕЖДЕ чем
|
||||
# инициировать recreate. Таймаут не блокирует деплой навсегда —
|
||||
# SIGTERM-drain (#1182 Phase 2/3a) + stop_grace_period 120s остаются
|
||||
# финальной страховкой для того, что не успело дойти до checkpoint'а.
|
||||
# 3) Startup-reap — сразу после recreate помечаем 'cancelled' любые
|
||||
# 'running'-строки, чей heartbeat не обновлялся с МОМЕНТА (по часам
|
||||
# самой БД — SELECT NOW(), без risk clock-skew раннера), взятого
|
||||
# непосредственно перед stop. Такие строки заведомо осиротели ЭТИМ
|
||||
# recreate'ом (старый контейнер физически не может писать heartbeat
|
||||
# после своей остановки) — не ждём 6h периодического reap_zombies().
|
||||
# 'cancelled' (не 'zombie') — честно отличает «убит деплоем» от
|
||||
# «непонятно завис» (последнее по-прежнему ловит только 6h-reaper).
|
||||
# Порог — по метке времени конкретного recreate, а не по фиксированному
|
||||
# интервалу: не зависит от heartbeat-каденса разных источников и не
|
||||
# рискует ложно отменить НЕ относящийся к этому recreate run (напр.
|
||||
# admin-triggered scrape внутри backend, если backend в этом деплое
|
||||
# не пересоздавался — его heartbeat продолжит расти после checkpoint'а).
|
||||
SERVICES="browser backend frontend"
|
||||
SCRAPER_STOP_TS=""
|
||||
if [ "${SCRAPER_CHANGED:-true}" = "true" ]; then
|
||||
echo "→ scraper paths changed — recreating tradein-scraper"
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml up -d --no-deps scraper
|
||||
echo "→ scraper paths changed — waiting for in-flight scrape_runs to drain (up to 5 min)"
|
||||
drained=""
|
||||
for i in $(seq 1 30); do
|
||||
# NB: не сливать "psql не ответил" с "0 running" — иначе неудачный
|
||||
# прогон психgl молча читается как «слито», и graceful drain
|
||||
# становится no-op именно в момент проблем с БД во время деплоя.
|
||||
running_count=""
|
||||
psql_out="$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc \
|
||||
"SELECT COUNT(*) FROM scrape_runs WHERE status='running';" 2>/dev/null)" \
|
||||
&& running_count="$(printf '%s' "$psql_out" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||
if [ -n "$running_count" ] && [ "$running_count" = "0" ]; then
|
||||
drained="yes"; break
|
||||
fi
|
||||
if [ -z "$running_count" ]; then
|
||||
echo " ...не удалось прочитать running_count (psql failed) — считаем как «ещё активен», жду 10s (попытка $i/30)"
|
||||
else
|
||||
echo " ...${running_count} активных run(ов) ещё бегут, жду 10s (попытка $i/30)"
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
if [ -n "$drained" ]; then
|
||||
echo "→ Активных run'ов нет — recreate scraper безопасен."
|
||||
else
|
||||
echo "WARNING: активные scrape_runs остались после 5 мин ожидания — recreate продолжится."
|
||||
echo " SIGTERM-drain (#1182) + stop_grace_period 120s постараются сохранить checkpoint;"
|
||||
echo " startup-reap ниже подчистит то, что не успеет."
|
||||
fi
|
||||
|
||||
# Checkpoint по часам БД (не раннера) прямо перед recreate.
|
||||
# NB: tr -d '[:space:]' сломан для timestamptz-литерала — убирает и
|
||||
# внутренний пробел между датой и временем ("2026-07-04 06:43" →
|
||||
# "2026-07-0406:43"), CAST(...AS timestamptz) на такое падает молча
|
||||
# (см. WARNING-фолбэк ниже). sed убирает только leading/trailing.
|
||||
SCRAPER_STOP_TS="$(docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -tAc "SELECT NOW();" 2>/dev/null | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" || SCRAPER_STOP_TS=""
|
||||
echo "→ scraper checkpoint ts (DB clock): ${SCRAPER_STOP_TS:-unknown}"
|
||||
|
||||
SERVICES="$SERVICES scraper"
|
||||
else
|
||||
echo "→ scraper unchanged — tradein-scraper left running (подхватит новый image при следующем своём рестарте)"
|
||||
fi
|
||||
|
||||
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
|
||||
echo "→ Startup-reap (#1951): помечаем orphaned running-строки, замороженные recreate'ом"
|
||||
# NB: psql `-c` НЕ поддерживает `:'var'`-подстановку (переменная доходит до
|
||||
# сервера как литерал → syntax error, см. комментарий выше про TRADEIN_READER_PASSWORD)
|
||||
# — поэтому подставляем bash-значением напрямую. SCRAPER_STOP_TS сгенерирован
|
||||
# самим Postgres (SELECT NOW()), не внешний ввод → безопасно.
|
||||
docker compose -p gendesign-tradein -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "${TRADEIN_POSTGRES_USER:-tradein}" -d tradein -v ON_ERROR_STOP=on -c "
|
||||
UPDATE scrape_runs
|
||||
SET status = 'cancelled',
|
||||
finished_at = NOW(),
|
||||
error = 'deploy #1951: tradein-scraper recreated mid-run (startup-reap, checkpoint ${SCRAPER_STOP_TS})'
|
||||
WHERE status = 'running'
|
||||
AND heartbeat_at < CAST('${SCRAPER_STOP_TS}' AS timestamptz);
|
||||
" || echo "WARNING: startup-reap query failed — orphaned runs (if any) fall back to the 6h zombie reaper"
|
||||
fi
|
||||
|
||||
# (5) `docker restart tradein-backend` БОЛЬШЕ НЕ НУЖЕН (issue #2216).
|
||||
# История (PR #493 / deploy 1156): backend раньше поднимался ПЕРЕД
|
||||
# миграциями, его lifespan-hook (ensure_fdw_user_mapping) падал с
|
||||
|
|
|
|||
|
|
@ -157,6 +157,11 @@ services:
|
|||
# Образ тот же что у backend (один Dockerfile), команда другая.
|
||||
# НЕ пересоздаётся при каждом backend-деплое — селективный up в deploy-tradein.yml
|
||||
# поднимает scraper только при изменениях scraper/infra путей (см. SCRAPER_CHANGED).
|
||||
# #1951: когда scraper ВСЁ ЖЕ пересоздаётся, deploy-tradein.yml (1) ждёт до 5 мин
|
||||
# graceful drain'а активных scrape_runs, (2) поднимает его АТОМАРНО одним compose
|
||||
# invocation вместе с browser/backend/frontend (а не отдельной командой позже), и
|
||||
# (3) сразу после recreate помечает 'cancelled' любые осиротевшие 'running'-строки
|
||||
# вместо ожидания 6h zombie-reaper'а. Все три меры — в deploy-скрипте, не здесь.
|
||||
scraper:
|
||||
image: ghcr.io/lekss361/gendesign-tradein-backend:${IMAGE_TAG:-latest}
|
||||
container_name: tradein-scraper
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue