restart-server: Wait until chain reload has completed.

We should not proceed and send client reload events until we know that
all of the server processes have updated to the latest version, or
they may reload into the old server version if they hit a Django
worker which has not yet restarted.

Because the logic controlling the number of workers is mildly complex,
and lives in Puppet, use the `uwsgi` Python bindings to know when the
process being reloaded is the last one, and use that to write out a
file signifying the success of the chain reload.  `restart-server`
awaits the creation of this file before proceeding.
This commit is contained in:
Alex Vandiver 2024-08-28 01:57:32 +00:00 committed by Tim Abbott
parent 3efc5ae1fd
commit 674ca1a95d
3 changed files with 34 additions and 0 deletions

View File

@ -84,6 +84,7 @@ module = [
"tlds.*", "tlds.*",
"twitter.*", "twitter.*",
"two_factor.*", "two_factor.*",
"uwsgi",
] ]
ignore_missing_imports = true ignore_missing_imports = true

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import contextlib
import logging import logging
import os import os
import pwd import pwd
@ -201,10 +202,19 @@ if has_application_server():
) )
if uwsgi_status.returncode == 0: if uwsgi_status.returncode == 0:
logging.info("Starting rolling restart of django server") logging.info("Starting rolling restart of django server")
with contextlib.suppress(FileNotFoundError):
os.unlink("/var/lib/zulip/django-workers.ready")
with open("/home/zulip/deployments/uwsgi-control", "w") as control_socket: with open("/home/zulip/deployments/uwsgi-control", "w") as control_socket:
# "c" is chain-reloading: # "c" is chain-reloading:
# https://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands # https://uwsgi-docs.readthedocs.io/en/latest/MasterFIFO.html#available-commands
control_socket.write("c") control_socket.write("c")
n = 0
while not os.path.exists("/var/lib/zulip/django-workers.ready"):
time.sleep(1)
n += 1
if n % 5 == 0:
logging.info("...")
logging.info("Chain reloading complete")
else: else:
logging.info("Starting django server") logging.info("Starting django server")
subprocess.check_call(["supervisorctl", "start", "zulip-django"]) subprocess.check_call(["supervisorctl", "start", "zulip-django"])

View File

@ -25,9 +25,11 @@ setup_path()
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "zproject.settings")
import contextlib
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
import orjson
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
try: try:
@ -67,6 +69,27 @@ try:
}, },
ignored_start_response, ignored_start_response,
) )
with contextlib.suppress(ModuleNotFoundError):
# The uwsgi module is only importable when running under
# uwsgi; development uses this file as well, but inside a
# pure-Python server. The surrounding contextmanager ensures
# that we don't bother with these steps if we're in
# development.
import uwsgi
if uwsgi.worker_id() == uwsgi.numproc:
# This is the last worker to load in the chain reload
with open("/var/lib/zulip/django-workers.ready", "wb") as f:
# The contents of this file are not read by restart-server
# in any way, but leave some useful information about the
# state of uwsgi.
f.write(
orjson.dumps(
uwsgi.workers(), option=orjson.OPT_INDENT_2, default=lambda e: e.decode()
),
)
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