Files
ForbiddenStarsApp/run.sh
T
NotBigGhostandClaude Opus 5 b4663796a3 uvicorn: ограничить graceful shutdown, чтобы SSE не вешал reload
После запуска dev сайт уходил в вечную загрузку без единой ошибки в логе. Открытая
вкладка держит SSE-поток /api/events, и сервер его сам не закрывает. При любой правке
.py (pull, переключение ветки, мёрж) uvicorn --reload останавливает старый процесс,
а тот в graceful shutdown ждёт закрытия всех соединений. Лимита по умолчанию нет,
поэтому ожидание длится вечно: новый процесс не стартует, слушающий сокет остаётся
у reloader'а, соединения принимаются в backlog и никем не обслуживаются. Запрос
/api/users/me висит, RequireAuth крутит спиннер, в логе только "Reloading...".

Воспроизведено тем же способом, каким запускает run.ps1 (uvicorn в отдельном окне):
при открытом SSE и тронутом .py /api/health не отвечал, хотя TCP-соединение
устанавливалось за 12 мс. Сервер ожил ровно в момент закрытия SSE.

Теперь uvicorn запускается с --timeout-graceful-shutdown: по истечении лимита он
отменяет висящие задачи запросов и доводит перезапуск до конца. В dev лимит 2 с, и
тот же сценарий отвечает 200 примерно через 4 с после правки файла. В entrypoint
лимит 10 с: в контейнере тот же механизм держал остановку до SIGKILL по
stop_grace_period (30 с), и lifespan-shutdown не выполнялся. Команды ручного запуска
в README дополнены тем же флагом.

#47

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6
2026-09-13 12:18:25 +03:00

101 lines
6.2 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# ╔═══════════════════════════════════════════════════════════════════════════╗
# ║ Единый лаунчер dev / test. Что запускать — решает APP_ENV из корневого .env ║
# ╚═══════════════════════════════════════════════════════════════════════════╝
# development → uvicorn --reload (бэк) + vite (фронт), нативно (Ctrl+C останавливает оба)
# test → docker compose прод-клон (порт 8080)
# production → лаунчером НЕ запускается (прод обособлен: docker compose up -d на Pi)
#
# LOCAL_PUBLIC=vps (только dev) поднимает SSH-туннель на VPS → https://forbidden-stars.ru
# test/prod выставляют себя сами через туннель-контейнер (см. docker-compose*.yml).
#
# Запуск: ./run.sh (при необходимости: chmod +x run.sh)
set -euo pipefail
root="$(cd "$(dirname "$0")" && pwd)"
envf="$root/.env"
[ -f "$envf" ] || { echo "Нет .env в корне. Скопируйте: cp .env.example .env"; exit 1; }
# Достаём значение ключа из .env (последнее присвоение, без комментариев/пробелов).
env_val() {
grep -E "^[[:space:]]*$1[[:space:]]*=" "$envf" | tail -n1 \
| sed -E 's/^[^=]*=[[:space:]]*//; s/[[:space:]#].*$//'
}
app_env="$(env_val APP_ENV | tr 'A-Z' 'a-z')"; app_env="${app_env:-development}"
local_public="$(env_val LOCAL_PUBLIC | tr 'A-Z' 'a-z')"; local_public="${local_public:-local}"
vps_host="$(env_val VPS_TUNNEL_HOST)"
vps_user="$(env_val VPS_TUNNEL_USER)"; vps_user="${vps_user:-tunnel}"
vps_port="$(env_val VPS_TUNNEL_PORT)"; vps_port="${vps_port:-9001}"
echo "APP_ENV = $app_env LOCAL_PUBLIC = $local_public"
# Поднимает SSH reverse-tunnel в фоне: VPS:$vps_port → localhost:$1 (если LOCAL_PUBLIC=vps).
start_tunnel() {
local local_port="$1"
if [ -z "$vps_host" ]; then
echo "LOCAL_PUBLIC=vps, но VPS_TUNNEL_HOST не задан — туннель пропущен."; return
fi
echo "SSH-туннель: forbidden-stars.ru (VPS:$vps_port) → localhost:$local_port …"
ssh -N -R "${vps_port}:localhost:${local_port}" \
-o ServerAliveInterval=30 -o ServerAliveCountMax=3 -o ExitOnForwardFailure=yes \
"${vps_user}@${vps_host}" &
tunnel=$!
trap 'kill "$tunnel" 2>/dev/null || true' EXIT INT TERM
echo " Публично: https://forbidden-stars.ru"
}
case "$app_env" in
development)
py="$root/backend/.venv/bin/python"
[ -x "$py" ] || py="$root/backend/.venv/Scripts/python.exe" # Git Bash на Windows
[ -x "$py" ] || { echo "Нет venv. Сначала: cd backend && python -m venv .venv && pip install -e '.[dev]'"; exit 1; }
[ -d "$root/frontend/node_modules" ] || { echo "Нет node_modules. Сначала: cd frontend && npm install"; exit 1; }
# Сначала схема: ветка могла принести миграцию, и без неё бэк поднимется на
# старой схеме и упадёт 500 на первом же обращении к новой таблице.
echo "Применяю миграции (alembic upgrade head)…"
mig_log="$( cd "$root/backend" && "$py" -m alembic upgrade head 2>&1 )" && mig_code=0 || mig_code=$?
if [ "$mig_code" -ne 0 ]; then
echo "$mig_log"
case "$mig_log" in
*"locate revision"*)
# В базе миграция, которой нет в этой ветке (вернулись с ветки задачи).
# Лишние таблицы старому коду не мешают — продолжаем запуск.
echo "База новее ветки — миграции пропущены." ;;
*)
echo "Миграции не применились — не запускаю. Исправьте ошибку выше и повторите."
exit 1 ;;
esac
fi
echo "Запуск бэка (uvicorn --reload) и фронта (vite)… Ctrl+C — остановить всё."
# --timeout-graceful-shutdown: открытая вкладка держит SSE-поток /api/events вечно, а при
# reload старый процесс ждёт закрытия ВСЕХ соединений — без лимита перезапуск не
# завершается, и сайт висит на загрузке без единой ошибки в логе.
( cd "$root/backend" && exec "$py" -m uvicorn app.main:app --reload --timeout-graceful-shutdown 2 ) &
back=$!
trap 'kill "$back" 2>/dev/null || true' EXIT INT TERM
echo " Бэк: http://127.0.0.1:8000 (Swagger: /api/docs)"
echo " Фронт: http://127.0.0.1:5173"
[ "$local_public" = "vps" ] && start_tunnel 5173
( cd "$root/frontend" && npm run dev )
;;
test)
docker info >/dev/null 2>&1 || { echo "Docker-демон недоступен — запусти Docker и повтори ./run.sh"; exit 1; }
echo "Сборка и запуск прод-клона в Docker (app + туннель в контейнере)…"
docker compose -f "$root/docker-compose.test.yml" up --build -d
echo " Публично: https://forbidden-stars.ru (Swagger: /api/docs)"
echo " (портов на хост нет — только через домен; туннель живёт внутри compose)"
echo " Логи: docker compose -f docker-compose.test.yml logs -f"
echo " Стоп: docker compose -f docker-compose.test.yml down -v"
;;
production)
echo "production лаунчером не запускается — прод обособлен."
echo "Деплой на Pi (из ветки main): docker compose up -d --build"
exit 1
;;
*)
echo "Неизвестный APP_ENV='$app_env'. Допустимо: development | test | production."
exit 1
;;
esac