From b4663796a39be8198cdf898a283d9494563711e4 Mon Sep 17 00:00:00 2001 From: NotBigGhost Date: Sun, 13 Sep 2026 12:18:25 +0300 Subject: [PATCH] =?UTF-8?q?uvicorn:=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=87=D0=B8=D1=82=D1=8C=20graceful=20shutdown,=20=D1=87=D1=82?= =?UTF-8?q?=D0=BE=D0=B1=D1=8B=20SSE=20=D0=BD=D0=B5=20=D0=B2=D0=B5=D1=88?= =?UTF-8?q?=D0=B0=D0=BB=20reload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После запуска 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 Claude-Session: https://claude.ai/code/session_01XfTsytzT6TojfmprRDKiV6 --- README.md | 10 +++++++--- backend/entrypoint.sh | 4 +++- deploy/README.md | 5 +++++ run.ps1 | 5 ++++- run.sh | 5 ++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 10aa7ce..bbe83dd 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ pip install -e ".[dev]" # .[dev] — один аргумент (пак Copy-Item ..\.env.example ..\.env # ЕДИНЫЙ .env лежит в КОРНЕ репозитория alembic upgrade head # применит миграции и сидинг python -m app.bootstrap # создаст/синхронизирует администратора из .env -uvicorn app.main:app --reload # http://localhost:8000 (Swagger: /api/docs) +uvicorn app.main:app --reload --timeout-graceful-shutdown 2 # http://localhost:8000 (Swagger: /api/docs) ``` **Windows cmd.exe** (здесь `&&` поддерживается): @@ -61,7 +61,7 @@ pip install -e ".[dev]" copy ..\.env.example ..\.env alembic upgrade head python -m app.bootstrap -uvicorn app.main:app --reload +uvicorn app.main:app --reload --timeout-graceful-shutdown 2 ``` **Linux / macOS / Git Bash:** @@ -72,9 +72,13 @@ pip install -e ".[dev]" cp ../.env.example ../.env alembic upgrade head python -m app.bootstrap -uvicorn app.main:app --reload +uvicorn app.main:app --reload --timeout-graceful-shutdown 2 ``` +> **`--timeout-graceful-shutdown` обязателен.** Открытая вкладка держит SSE-поток +> `/api/events`, и без лимита `--reload` ждёт его закрытия вечно — сайт висит на загрузке, +> а в логе только `Reloading...`. + > **Единый `.env` — в корне репозитория** (`ForbidenStarsApp/.env`), рядом с `.env.example`. > Его читают и бэкенд (через абсолютный путь, независимо от рабочей папки), и `docker compose`. > Файл — локальный, на каждой машине свой (dev/prod различаются строкой `APP_ENV`). diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index a513b62..3425430 100644 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -10,5 +10,7 @@ python -m app.bootstrap echo "[entrypoint] Запуск сервера…" # --proxy-headers + доверие к X-Forwarded-* от реверс-прокси (Caddy на VPS): # чтобы приложение знало, что снаружи запрос пришёл по HTTPS. +# --timeout-graceful-shutdown: SSE-потоки /api/events сами не закрываются, и без лимита +# остановка ждала бы их до SIGKILL по stop_grace_period (30 с) — без lifespan-shutdown. exec uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1 \ - --proxy-headers --forwarded-allow-ips="*" + --proxy-headers --forwarded-allow-ips="*" --timeout-graceful-shutdown 10 diff --git a/deploy/README.md b/deploy/README.md index fe335ca..4823166 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -57,3 +57,8 @@ HTTPS твоими сертификатами и проксирует трафи Шина событий — **in-memory**, рассчитана на один процесс (uvicorn `--workers 1`, как в контейнере). Если когда-нибудь поднимешь несколько воркеров/реплик — шину нужно вынести во внешний брокер (Redis pub/sub), иначе события увидит только тот воркер, что принял мутацию. + +SSE-поток сам не закрывается, поэтому uvicorn запускается с `--timeout-graceful-shutdown` +(`entrypoint.sh`, `run.*`). Без него остановка ждёт закрытия всех соединений: в dev +`--reload` при открытой вкладке висит вечно, в контейнере остановку обрывает только SIGKILL +по `stop_grace_period`. diff --git a/run.ps1 b/run.ps1 index 419da7d..ecec9cf 100644 --- a/run.ps1 +++ b/run.ps1 @@ -112,9 +112,12 @@ switch ($appEnv) { } Write-Host "Starting backend (uvicorn --reload) and frontend (vite) in separate windows..." -ForegroundColor Green + # --timeout-graceful-shutdown: an open tab keeps the SSE stream /api/events forever, + # and on reload the old process waits for ALL connections to close - with no limit + # the reload never finishes and the site hangs on loading with nothing in the log. Start-Process powershell -ArgumentList @( "-NoExit", "-Command", - "Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload" + "Set-Location '$backend'; & '$py' -m uvicorn app.main:app --reload --timeout-graceful-shutdown 2" ) Start-Process powershell -ArgumentList @( "-NoExit", "-Command", diff --git a/run.sh b/run.sh index 047d691..231591a 100644 --- a/run.sh +++ b/run.sh @@ -68,7 +68,10 @@ case "$app_env" in fi echo "Запуск бэка (uvicorn --reload) и фронта (vite)… Ctrl+C — остановить всё." - ( cd "$root/backend" && exec "$py" -m uvicorn app.main:app --reload ) & + # --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)"