From a55f276965d242f5ca763e8763cd8b85263d0253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20De=20la=20Torre=20Vico?= Date: Thu, 11 Jun 2026 16:15:52 +0200 Subject: [PATCH] feat(api): serve the API through the gunicorn ASGI worker Run gunicorn with the native asgi worker against config.asgi so SSE streams are parked on the event loop instead of holding a sync worker per open connection; sync CRUD views keep running in the thread-sensitive executor. Disable preload under DEBUG so dev reload picks up edited code, and point the dev and prod entrypoints at the ASGI application. --- api/docker-entrypoint.sh | 12 +++++++++--- api/src/backend/config/guniconf.py | 9 +++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/api/docker-entrypoint.sh b/api/docker-entrypoint.sh index 9b2964b479..e077a3bfd6 100755 --- a/api/docker-entrypoint.sh +++ b/api/docker-entrypoint.sh @@ -21,13 +21,19 @@ apply_fixtures() { } start_dev_server() { - echo "Starting the development server..." - exec uv run python manage.py runserver 0.0.0.0:"${DJANGO_PORT:-8080}" + echo "Starting the development server (Gunicorn ASGI, debug + reload)..." + # Same server/worker as prod (config.asgi via the native `asgi` worker), so + # SSE streams run on the event loop exactly as they do in production. DEBUG is + # on so guniconf's `reload = DEBUG` hot-reloads edited code (and flips + # `preload_app` off so reload actually takes). + export DJANGO_DEBUG="${DJANGO_DEBUG:-True}" + export DJANGO_BIND_ADDRESS="${DJANGO_BIND_ADDRESS:-0.0.0.0}" + exec uv run gunicorn -c config/guniconf.py config.asgi:application } start_prod_server() { echo "Starting the Gunicorn server..." - exec uv run gunicorn -c config/guniconf.py config.wsgi:application + exec uv run gunicorn -c config/guniconf.py config.asgi:application } resolve_worker_hostname() { diff --git a/api/src/backend/config/guniconf.py b/api/src/backend/config/guniconf.py index a16c8de9a0..6f07913e81 100644 --- a/api/src/backend/config/guniconf.py +++ b/api/src/backend/config/guniconf.py @@ -25,6 +25,15 @@ bind = f"{BIND_ADDRESS}:{PORT}" workers = env.int("DJANGO_WORKERS", default=multiprocessing.cpu_count() * 2 + 1) reload = DEBUG +# Native ASGI worker (gunicorn 24+). Required so SSE endpoints can keep the +# event loop alive while waiting for events. +worker_class = env("DJANGO_WORKER_CLASS", default="asgi") + +# Preload the application before forking workers in production: the app is +# imported once in the master and workers fork from it. In development, disable +# preload so the server restarts on code changes. +preload_app = not DEBUG + # Logging logconfig_dict = DJANGO_LOGGERS gunicorn_logger = logging.getLogger(BackendLogger.GUNICORN)