uwsgi: Force Django load before returning the uwsgi worker function.

Django lazy-loads much of its modules, including the application's.
This defers the time to during the first request it serves.  When
doing rolling-restarts, this means that the worker is marked "ready"
despite having multiple seconds more work to do.  With small numbers
of workers, this causes a significant capacity drop, since effectively
more than one worker can be still reloading at a time.  It also
results in poor user experience for the requests which are unlucky
enough to be served first.

Use the technique detailed in the uwsgi documentation[^1] to fake a
request during initial application load, which forces the application
to be loaded.

[^1]: https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#dealing-with-ultra-lazy-apps-like-django
This commit is contained in:
Alex Vandiver 2024-08-23 16:12:44 +00:00 committed by Tim Abbott
parent 8589becc48
commit eef65d7e30
1 changed files with 34 additions and 0 deletions

View File

@ -25,6 +25,9 @@ setup_path()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings")
from collections.abc import Callable
from typing import Any
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
try: try:
@ -33,6 +36,37 @@ try:
# setting points here. # setting points here.
application = get_wsgi_application() application = get_wsgi_application()
# We force loading of the main parts of the application now, by
# handing it a fake request, rather than have to pay that price
# during the first request served by this process. Hitting the
# /health endpoint will not only load Django and all of the views
# (by loading the URL dispatcher) but will also force open any
# lazy-loaded service connections.
#
# The return value (and thus response status) of this healthcheck
# request is ignored, so we do return the application handler even
# if connections are not fully available yet. This at least
# allows application logging to handle any such errors, instead of
# arcane errors from uwsgi not being able to load its handler
# function.
def ignored_start_response(
status: str, headers: list[tuple[str, str]], exc_info: Any = None, /
) -> Callable[[bytes], object]:
return lambda x: None
application(
{
"REQUEST_METHOD": "GET",
"SERVER_NAME": "127.0.0.1",
"SERVER_PORT": "443",
"PATH_INFO": "/health",
"REMOTE_ADDR": "127.0.0.1",
"wsgi.input": sys.stdin,
"wsgi.url_scheme": "https",
},
ignored_start_response,
)
except Exception: except Exception:
# If /etc/zulip/settings.py contains invalid syntax, Django # If /etc/zulip/settings.py contains invalid syntax, Django
# initialization will fail in django.setup(). In this case, our # initialization will fail in django.setup(). In this case, our