From 1b1977efa317116aebc9cd3ce19c1b428358a9e2 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Wed, 2 Sep 2026 16:58:40 +0500 Subject: [PATCH] =?UTF-8?q?fix(deploy):=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20=D0=B3=D0=B5=D0=B9?= =?UTF-8?q?=D1=82=D0=B0=20=D0=9F=D0=A2=D0=98=D0=A6=D0=AB=20=E2=80=94=20?= =?UTF-8?q?=D1=82=D0=B0=D0=B9=D0=BC=D0=B0=D1=83=D1=82=20=D1=81=D0=B5=D1=81?= =?UTF-8?q?=D1=81=D0=B8=D0=B8,=20crash-loop,=20=D0=BC=D0=BD=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=B2=D0=B5=D0=BD=D0=BD=D1=8B=D0=B9=20id=20(?= =?UTF-8?q?#3324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit command_timeout: 30m — дефолт appleboy/ssh-action 10m короче worst-case гейта (~17 мин), сессию убило бы посреди диагноза и авария читалась бы обрывом связи. Деградация «нет health-конфига» требовала лишь running дважды: crash-loop с временем жизни больше паузы проходил как стабильный (обе проверки видят running, просто это разные жизни контейнера). Теперь сверяется RestartCount до/после окна 15 с; окно учитывается в счётчике ожидания, иначе таймаут 240 с растянулся бы на ~24 мин и упёрся в command_timeout. cid(): `ps -aq` возвращает несколько id при залежавшемся exited-контейнере → docker inspect падает → пустой статус → ложный красный. tail -n1. worker healthcheck: убран `2>&1` (глушил причину, которую деплой печатает из .State.Health.Log), retries 3→5 — при interval 60s тройка промахов = 3 минуты, столько длится обычный флап Redis, а из unhealthy контейнер сам не выходит. --- .forgejo/workflows/deploy.yml | 31 ++++++++++++++++++++++++++----- docker-compose.prod.yml | 11 ++++++++--- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index a6ce13a3..9b2c1866 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -596,6 +596,11 @@ jobs: # #3029: подлинность хоста. Секрет НЕ задан → пустая строка → easyssh-proxy # оставляет ssh.InsecureIgnoreHostKey(), то есть сегодняшнее поведение. fingerprint: ${{ secrets.DEPLOY_SSH_FINGERPRINT }} + # #3324: дефолт appleboy/ssh-action — command_timeout 10m, а worst-case + # гейта в конце скрипта ~17 мин (4 сервиса × 240s ожидания healthy + + # фронт + diagnose). Сессию убило бы посреди печати диагноза, и авария + # выглядела бы обрывом связи, а не мёртвым контейнером. + command_timeout: 30m 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 @@ -1052,7 +1057,11 @@ jobs: # ломая порядок «миграции до подъёма кода». Читаем состояние явно. # Все проверки выполняются ДО выхода (не падаем на первой) — один # прогон обязан показать ВСЕ поломанные сервисы, а не первый по списку. - cid() { docker compose -p gendesign -f docker-compose.prod.yml ps -aq "$1" 2>/dev/null || true; } + # tail -n1: у сервиса может остаться залежавшийся exited-контейнер, и + # тогда `ps -aq` вернёт НЕСКОЛЬКО id через \n — `docker inspect` с таким + # аргументом падает, статус приходит пустым и гейт краснеет на ровном + # месте. Последний id — самый свежий контейнер сервиса. + cid() { docker compose -p gendesign -f docker-compose.prod.yml ps -aq "$1" 2>/dev/null | tail -n1 || true; } diagnose() { # $1 сервис, $2 id контейнера (может быть пустым) local svc="$1" c="$2" @@ -1070,7 +1079,7 @@ jobs: } wait_healthy() { # $1 сервис, $2 таймаут, с - local svc="$1" deadline="$2" c="" status="" alive="" waited=0 + local svc="$1" deadline="$2" c="" status="" alive="" r0="" r1="" waited=0 while [ "$waited" -lt "$deadline" ]; do c="$(cid "$svc")" if [ -n "$c" ]; then @@ -1086,12 +1095,24 @@ jobs: # worker, пропущенный guard'ом #3029). Валить деплой за это # нельзя, но и молчать нельзя: падаем на «стабильный running» # (двойное чтение, как tgbot/scraper в deploy-tradein.yml). - sleep 3 + # Одного `running` дважды НЕДОСТАТОЧНО: crash-loop с временем + # жизни больше паузы читается как «стабилен» — контейнер оба + # раза running, просто это разные его жизни. Поэтому вместе со + # статусом сверяем RestartCount: изменился за окно = именно + # тот дефект, ради которого этот гейт и писался. + r0="$(docker inspect -f '{{.RestartCount}}' "$c" 2>/dev/null || echo '')" + sleep 15 alive="$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null || echo unknown)" - if [ "$alive" = "running" ]; then - echo "→ $svc: healthcheck не сконфигурирован (контейнер не пересоздавался), running стабилен" + r1="$(docker inspect -f '{{.RestartCount}}' "$c" 2>/dev/null || echo '')" + if [ "$alive" = "running" ] && [ -n "$r0" ] && [ "$r0" = "$r1" ]; then + echo "→ $svc: healthcheck не сконфигурирован (контейнер не пересоздавался), running стабилен (RestartCount=$r0 не изменился за 15s)" return 0 fi + # waited растёт на длину ЭТОЙ паузы тоже — иначе таймаут + # 240s превратился бы в ~24 минуты реального ожидания и упёрся + # бы в command_timeout SSH-сессии. + waited=$((waited + 15)) + echo " $svc: running нестабилен — status='$alive', RestartCount ${r0:-<нет>}→${r1:-<нет>} (контейнер перезапускался внутри окна наблюдения); продолжаю ждать" ;; esac fi diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 76e9dd00..03a76d08 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -325,12 +325,17 @@ services: # Интервал 60s (не 30s как у backend): каждая проба — отдельный запуск # celery-CLI с импортом приложения, дешёвым его не назовёшь. # start_period 90s: холодный старт worker'а с Chromium-образа заметно - # медленнее backend'а. 3 промаха подряд (~3 мин) → unhealthy. + # медленнее backend'а. + # stderr НЕ глушим: docker хранит вывод пробы в .State.Health.Log, и деплой + # печатает его в диагнозе — с `2>&1` там была бы пустота вместо причины. + # retries 5 (не 3): при interval 60s тройка промахов = 3 минуты, столько + # длится обычный флап Redis, а из unhealthy контейнер сам не выходит по + # restart-политике — следующий деплой краснел бы за исправный воркер. healthcheck: - test: ["CMD-SHELL", "celery -A app.workers.celery_app inspect ping -d celery@$$(hostname) -t 10 >/dev/null 2>&1"] + test: ["CMD-SHELL", "celery -A app.workers.celery_app inspect ping -d celery@$$(hostname) -t 10 >/dev/null"] interval: 60s timeout: 30s - retries: 3 + retries: 5 start_period: 90s # #976 cross-DB ETL tradein→gendesign: worker запускает etl_newbuilding_crossload task, # которому нужен прямой TCP-доступ к tradein-postgres через gendesign_shared.