fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
58 lines
2.1 KiB
Python
58 lines
2.1 KiB
Python
"""Gunicorn worker that bounds uvicorn's graceful-shutdown drain.
|
|
|
|
``uvicorn_worker`` doesn't forward gunicorn's ``--graceful-timeout`` to uvicorn's
|
|
``timeout_graceful_shutdown``, so after a ``max_requests`` recycle the drain is
|
|
unbounded: a held-open SSE/long-poll connection (a Flask/WSGI generator in
|
|
a2wsgi's thread pool, which asyncio can't cancel) hangs the worker until the
|
|
``--timeout`` watchdog SIGKILLs it (mislabeled "Perhaps out of memory?").
|
|
|
|
This worker bounds the drain (``timeout_graceful_shutdown`` from settings) and
|
|
raises the shutdown flag at drain start — including the signal-less
|
|
``max_requests`` path — so generators stop within one poll tick. Wire in via
|
|
``-k application.gunicorn_worker.BoundedDrainUvicornWorker``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import sys
|
|
from typing import Any
|
|
|
|
from gunicorn.arbiter import Arbiter
|
|
from uvicorn.server import Server
|
|
from uvicorn_worker import UvicornWorker
|
|
|
|
from application.core.settings import settings
|
|
from application.core.shutdown import begin_shutdown
|
|
|
|
|
|
class _ShutdownAwareServer(Server):
|
|
"""uvicorn ``Server`` that raises the shutdown flag when a drain starts.
|
|
|
|
``serve()`` always calls ``shutdown()`` when its loop exits — including the
|
|
signal-less ``max_requests`` recycle — so this is the reliable hook point.
|
|
"""
|
|
|
|
async def shutdown(self, sockets: list[socket.socket] | None = None) -> None:
|
|
begin_shutdown()
|
|
await super().shutdown(sockets=sockets)
|
|
|
|
|
|
class BoundedDrainUvicornWorker(UvicornWorker):
|
|
"""UvicornWorker with a bounded drain and shutdown-aware streaming."""
|
|
|
|
CONFIG_KWARGS: dict[str, Any] = {
|
|
"loop": "auto",
|
|
"http": "auto",
|
|
"timeout_graceful_shutdown": settings.GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS,
|
|
}
|
|
|
|
async def _serve(self) -> None:
|
|
# Mirrors UvicornWorker._serve but swaps in the shutdown-aware Server.
|
|
self.config.app = self.wsgi
|
|
server = _ShutdownAwareServer(config=self.config)
|
|
self._install_sigquit_handler()
|
|
await server.serve(sockets=self.sockets)
|
|
if not server.started:
|
|
sys.exit(Arbiter.WORKER_BOOT_ERROR)
|