Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

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)