chore(ci): деплой не убивает worker посреди скрап-прогона (#3084)
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-worker (push) Successful in 43s
Deploy / build-backend (push) Successful in 44s
Deploy / build-frontend (push) Successful in 42s
Deploy / deploy (push) Successful in 1m41s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
All checks were successful
Deploy / changes (push) Successful in 9s
Deploy / deploy-caddy (push) Has been skipped
Deploy / build-worker (push) Successful in 43s
Deploy / build-backend (push) Successful in 44s
Deploy / build-frontend (push) Successful in 42s
Deploy / deploy (push) Successful in 1m41s
Deploy / deploy-status (push) Successful in 1s
Deploy / perimeter-smoke (push) Successful in 9s
This commit is contained in:
parent
536137d460
commit
17d23cfaae
1 changed files with 109 additions and 3 deletions
|
|
@ -583,6 +583,11 @@ jobs:
|
|||
# own-portfolio каннибализации. Non-sensitive (публичные id) → actions
|
||||
# variable. UNSET → каннибализация отдаёт proxy (фича дормант).
|
||||
OWN_DEVELOPER_IDS: ${{ vars.OWN_DEVELOPER_IDS }}
|
||||
# #3029: guard на пересоздание worker'а во время активного скрап-прогона
|
||||
# (временная заплатка до чекпоинтов #3074). Откат — переменная репозитория
|
||||
# в 'off', без коммита. Non-sensitive → actions variable, не secret.
|
||||
WORKER_RECREATE_GUARD: ${{ vars.WORKER_RECREATE_GUARD }}
|
||||
WORKER_GUARD_MAX_SKIP_H: ${{ vars.WORKER_GUARD_MAX_SKIP_H }}
|
||||
with:
|
||||
host: ${{ secrets.DEPLOY_HOST }}
|
||||
username: ${{ secrets.DEPLOY_USER }}
|
||||
|
|
@ -591,7 +596,7 @@ jobs:
|
|||
# #3029: подлинность хоста. Секрет НЕ задан → пустая строка → easyssh-proxy
|
||||
# оставляет ssh.InsecureIgnoreHostKey(), то есть сегодняшнее поведение.
|
||||
fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }}
|
||||
envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT,GLITCHTIP_BACKEND_DSN,OBJECTIVE_API_KEY,OPENAI_API_KEY,LLM_ENABLED,OWN_DEVELOPER_IDS
|
||||
envs: IMAGE_TAG,SENTRY_RELEASE_VAL,GHCR_PAT,GLITCHTIP_BACKEND_DSN,OBJECTIVE_API_KEY,OPENAI_API_KEY,LLM_ENABLED,OWN_DEVELOPER_IDS,WORKER_RECREATE_GUARD,WORKER_GUARD_MAX_SKIP_H
|
||||
script: |
|
||||
set -euo pipefail
|
||||
# #2950: взаимное исключение докер-секции двух прод-деплоев.
|
||||
|
|
@ -725,6 +730,19 @@ jobs:
|
|||
# Apply pending SQL migrations
|
||||
set -a; source .env; set +a
|
||||
|
||||
# #3029: checkpoint по часам БД ДО миграций — worker-guard ниже (после
|
||||
# force-recreate блока) сверяет его с applied_at, чтобы честно
|
||||
# предупредить «worker на старом коде + новая схема», если recreate
|
||||
# worker'а был пропущен именно в деплое, где данные схемы поменялись.
|
||||
# `tr -d '[:space:]'` здесь СЛОМАН бы CAST ниже: NOW() отдаёт
|
||||
# "2026-08-24 09:12:33+00" с пробелом ВНУТРИ значения (дата/время),
|
||||
# который [:space:] тоже вырезает → "2026-08-2409:12:33+00" не
|
||||
# парсится как timestamptz. Убираем только CR/LF (psql -tA не
|
||||
# добавляет ведущих/хвостовых пробелов, только trailing \n).
|
||||
DEPLOY_MIGRATIONS_START_TS="$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc "SELECT NOW();" 2>/dev/null | tr -d '\r\n')" \
|
||||
|| DEPLOY_MIGRATIONS_START_TS=""
|
||||
|
||||
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -v ON_ERROR_STOP=on -c "
|
||||
CREATE TABLE IF NOT EXISTS _schema_migrations (
|
||||
|
|
@ -862,7 +880,17 @@ jobs:
|
|||
# Cache-friendly: первый build ~30s, последующие 1-3s если файлы не менялись.
|
||||
docker compose -p gendesign -f docker-compose.prod.yml build glitchtip-auth-forwarder
|
||||
|
||||
docker compose -p gendesign -f docker-compose.prod.yml up -d
|
||||
# #3029 fail-safe review fix: голый `up -d` без списка сервисов сам
|
||||
# пересоздаёт ЛЮБОЙ сервис с изменившимся image — включая worker,
|
||||
# ДО того как guard ниже (~896) успевает сравнить pulled vs running.
|
||||
# К моменту проверки они уже совпадают (worker только что
|
||||
# пересоздан этим самым up -d) → guard видит "не изменился" и
|
||||
# печатает no-op, хотя worker уже убит и прогон уже потерян.
|
||||
# Фикс: явно исключаем worker из этого bulk up -d — его recreate
|
||||
# решается ТОЛЬКО guard-блоком ниже (строка ~977), который видит
|
||||
# ещё не тронутый running_worker_image.
|
||||
UP_SERVICES="$(docker compose -p gendesign -f docker-compose.prod.yml config --services | grep -v '^worker$')"
|
||||
docker compose -p gendesign -f docker-compose.prod.yml up -d $UP_SERVICES
|
||||
|
||||
# Defense: ensure postgres is in gendesign_shared network for tradein FDW.
|
||||
# `compose up -d` should detect networks: shared addition and recreate
|
||||
|
|
@ -883,8 +911,86 @@ jobs:
|
|||
# требуют --force-recreate — обычный `up -d` не перечитывает env_file
|
||||
# если только image не сменился. На deploy где меняется только runtime
|
||||
# без backend image change — без этого backend остаётся со старым DSN.
|
||||
#
|
||||
# #3029: worker исключён из безусловного recreate. Временная заплатка
|
||||
# до чекпоинтов (#3074) — стиль (digest-сверка pulled vs running,
|
||||
# $SERVICES) как SCRAPER_RECREATE в deploy-tradein.yml, но здесь
|
||||
# расхождение digest после skip уходит в WARNING, не в exit 1 (там
|
||||
# scraper не имеет второго потребителя схемы; здесь пропуск recreate
|
||||
# worker'а может оставить его читать старую версию схемы — риск, а не
|
||||
# ошибка деплоя). backend/beat пересоздаются безусловно, как раньше.
|
||||
WORKER_SERVICES="backend beat"
|
||||
if [ "${WORKER_RECREATE_GUARD:-on}" != "on" ]; then
|
||||
echo "→ WORKER_RECREATE_GUARD=off — безусловное пересоздание worker'а (fallback на старое поведение)"
|
||||
WORKER_SERVICES="$WORKER_SERVICES worker"
|
||||
else
|
||||
pulled_worker_image=$(docker image inspect -f '{{.Id}}' "ghcr.io/lekss361/gendesign-worker:$IMAGE_TAG" 2>/dev/null || echo "")
|
||||
running_worker_image=$(docker inspect -f '{{.Image}}' "$(docker compose -p gendesign -f docker-compose.prod.yml ps -q worker)" 2>/dev/null || echo "")
|
||||
if [ -z "$pulled_worker_image" ] || [ -z "$running_worker_image" ]; then
|
||||
echo "WARNING (#3029): не удалось прочитать worker image id (pulled='$pulled_worker_image' running='$running_worker_image') — детект не отработал, fail-safe = пересоздаём worker как обычно."
|
||||
WORKER_SERVICES="$WORKER_SERVICES worker"
|
||||
elif [ "$pulled_worker_image" = "$running_worker_image" ]; then
|
||||
echo "→ образ worker'а не изменился ($pulled_worker_image) — пересоздание и так no-op, worker в recreate"
|
||||
WORKER_SERVICES="$WORKER_SERVICES worker"
|
||||
else
|
||||
# Оба трекера прогонов — строки в БД (lifecycle.py:108 kn_scrape_runs,
|
||||
# lifecycle.py:267 objective_scrape_runs), литералы статуса сверены с
|
||||
# кодом: 'running' в обеих таблицах. Анти-зомби: считаем только
|
||||
# прогоны свежее WORKER_GUARD_MAX_SKIP_H часов (started_at /
|
||||
# heartbeat_at) — иначе зависший навечно 'running' блокировал бы
|
||||
# recreate worker'а бесконечно.
|
||||
# NB: `assignment="$(...)" && next=...` (не отдельная строка) — под
|
||||
# `set -euo pipefail` (шапка скрипта) присвоение, упавшее КАК
|
||||
# ПОСЛЕДНЯЯ команда своего стейтмента, роняет весь деплой. Внутри
|
||||
# AND-списка (не последним звеном) — нет, ошибка молча даёт пустой
|
||||
# $kn_count/$obj_count, что и проверяем ниже. Тот же приём —
|
||||
# deploy-tradein.yml psql_out/running_count.
|
||||
guard_max_h="${WORKER_GUARD_MAX_SKIP_H:-6}"
|
||||
kn_count=""
|
||||
kn_out="$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
|
||||
"SELECT COUNT(*) FROM kn_scrape_runs WHERE status='running' AND COALESCE(heartbeat_at, started_at) > NOW() - CAST('${guard_max_h} hours' AS interval);" 2>/dev/null)" \
|
||||
&& kn_count="$(printf '%s' "$kn_out" | tr -d '[:space:]')"
|
||||
obj_count=""
|
||||
obj_out="$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
|
||||
"SELECT COUNT(*) FROM objective_scrape_runs WHERE status='running' AND COALESCE(heartbeat_at, started_at) > NOW() - CAST('${guard_max_h} hours' AS interval);" 2>/dev/null)" \
|
||||
&& obj_count="$(printf '%s' "$obj_out" | tr -d '[:space:]')"
|
||||
if ! printf '%s' "$kn_count" | grep -qE '^[0-9]+$' \
|
||||
|| ! printf '%s' "$obj_count" | grep -qE '^[0-9]+$'; then
|
||||
echo "WARNING (#3029): не удалось прочитать running-прогоны (psql молчит/пусто/не число) — детект не отработал, fail-safe = пересоздаём worker как обычно."
|
||||
WORKER_SERVICES="$WORKER_SERVICES worker"
|
||||
else
|
||||
total_running=$((kn_count + obj_count))
|
||||
if [ "$total_running" -gt 0 ]; then
|
||||
echo "!!! WORKER RECREATE SKIPPED (#3029) — ${total_running} running runs, worker остаётся на образе ${running_worker_image} !!!"
|
||||
echo "WARNING (#3029): worker digest разошёлся с pulled — running=${running_worker_image} pulled=${pulled_worker_image}"
|
||||
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
|
||||
"SELECT 'kn run_id=' || run_id || ' started_at=' || started_at FROM kn_scrape_runs WHERE status='running' AND COALESCE(heartbeat_at, started_at) > NOW() - CAST('${guard_max_h} hours' AS interval) ORDER BY started_at ASC;" 2>/dev/null || true
|
||||
docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
|
||||
"SELECT 'objective run_id=' || run_id || ' started_at=' || started_at FROM objective_scrape_runs WHERE status='running' AND COALESCE(heartbeat_at, started_at) > NOW() - CAST('${guard_max_h} hours' AS interval) ORDER BY started_at ASC;" 2>/dev/null || true
|
||||
if [ -n "$DEPLOY_MIGRATIONS_START_TS" ]; then
|
||||
migrations_since=""
|
||||
migrations_since=$(docker compose -p gendesign -f docker-compose.prod.yml exec -T postgres \
|
||||
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -tAc \
|
||||
"SELECT COUNT(*) FROM _schema_migrations WHERE applied_at > CAST('${DEPLOY_MIGRATIONS_START_TS}' AS timestamptz);" 2>/dev/null | tr -d '[:space:]') \
|
||||
|| migrations_since=""
|
||||
if printf '%s' "$migrations_since" | grep -qE '^[0-9]+$' && [ "$migrations_since" -gt 0 ]; then
|
||||
echo "WARNING (#3029): WORKER НА СТАРОМ КОДЕ + НОВАЯ СХЕМА — в этом деплое применились ${migrations_since} data/sql/** миграций, а worker остался на старом образе. Нужен ручной recreate после прогона: docker compose -p gendesign -f docker-compose.prod.yml up -d --force-recreate --no-deps worker"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "→ активных прогонов (kn/objective) нет — worker пересоздаётся вместе с backend/beat"
|
||||
WORKER_SERVICES="$WORKER_SERVICES worker"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
docker compose -p gendesign -f docker-compose.prod.yml up -d \
|
||||
--force-recreate --no-deps backend worker beat
|
||||
--force-recreate --no-deps $WORKER_SERVICES
|
||||
|
||||
# Caddy: force-recreate чтобы подхватить изменения в Caddyfile
|
||||
# И в особенности новые volume mounts из docker-compose.prod.yml
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue