Skip to content

Commit 4171311

Browse files
committed
systemd: send MAINPID updates on re-exec
1 parent 63a54b0 commit 4171311

File tree

3 files changed

+34
-10
lines changed

3 files changed

+34
-10
lines changed

gunicorn/arbiter.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ def __init__(self, app):
6767

6868
cwd = util.getcwd()
6969

70-
args = sys.argv[:]
71-
args.insert(0, sys.executable)
70+
if sys.version_info < (3, 10):
71+
args = sys.argv[:]
72+
args.insert(0, sys.executable)
73+
else:
74+
args = sys.orig_argv[:]
7275

7376
# init start context
7477
self.START_CTX = {
@@ -159,7 +162,7 @@ def start(self):
159162
self.log.debug("Arbiter booted")
160163
self.log.info("Listening at: %s (%s)", listeners_str, self.pid)
161164
self.log.info("Using worker: %s", self.cfg.worker_class_str)
162-
systemd.sd_notify("READY=1\nSTATUS=Gunicorn arbiter booted", self.log)
165+
systemd.sd_notify("READY=1\nSTATUS=Gunicorn arbiter booted\n", self.log)
163166

164167
# check worker class requirements
165168
if hasattr(self.worker_class, "check_config"):
@@ -251,7 +254,10 @@ def handle_hup(self):
251254
- Gracefully shutdown the old worker processes
252255
"""
253256
self.log.info("Hang up: %s", self.master_name)
257+
systemd.sd_notify("RELOADING=1\nSTATUS=Gunicorn arbiter reloading..\n", self.log)
254258
self.reload()
259+
# possibly premature, newly launched workers might have failed
260+
systemd.sd_notify("READY=1\nSTATUS=Gunicorn arbiter reloaded\n", self.log)
255261

256262
def handle_term(self):
257263
"SIGTERM handling"
@@ -327,6 +333,8 @@ def maybe_promote_master(self):
327333
self.pidfile.rename(self.cfg.pidfile)
328334
# reset proctitle
329335
util._setproctitle("master [%s]" % self.proc_name)
336+
# MAINPID does not change here, it was already set on fork
337+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter promoted\n" % (os.getpid(), ), self.log)
330338

331339
def wakeup(self):
332340
"""\
@@ -432,7 +440,10 @@ def reexec(self):
432440
os.chdir(self.START_CTX['cwd'])
433441

434442
# exec the process using the original environment
435-
os.execvpe(self.START_CTX[0], self.START_CTX['args'], environ)
443+
self.log.debug("exe=%r argv=%r" % (self.START_CTX[0], self.START_CTX['args']))
444+
# let systemd know are are in control
445+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec\n" % (master_pid, ), self.log)
446+
os.execve(self.START_CTX[0], self.START_CTX['args'], environ)
436447

437448
def reload(self):
438449
old_address = self.cfg.address
@@ -522,6 +533,8 @@ def reap_workers(self):
522533
if self.reexec_pid == wpid:
523534
self.reexec_pid = 0
524535
self.log.info("Master exited before promotion.")
536+
# let systemd know we are (back) in control
537+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec aborted\n" % (os.getpid(), ), self.log)
525538
continue
526539
else:
527540
worker = self.WORKERS.pop(wpid, None)

gunicorn/systemd.py

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import os
66
import socket
7+
import time
78

89
SD_LISTEN_FDS_START = 3
910

@@ -66,6 +67,13 @@ def sd_notify(state, logger, unset_environment=False):
6667
if addr[0] == '@':
6768
addr = '\0' + addr[1:]
6869
sock.connect(addr)
70+
assert state.endswith("\n")
71+
if "RELOADING" in state: # broad, but systemd man promises tolerating
72+
# wrong clock on some platforms.. but this is only needed on Linux
73+
# nsec = 10**-9
74+
# usec = 10**-6
75+
state += "MONOTONIC_USEC=%d\n" % (1_000*time.monotonic_ns(), )
76+
logger.debug("sd_notify: %r" % (state, ))
6977
sock.sendall(state.encode('utf-8'))
7078
except Exception:
7179
logger.debug("Exception while invoking sd_notify()", exc_info=True)

tests/test_arbiter.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,27 @@ def test_arbiter_stop_does_not_unlink_when_using_reuse_port(close_sockets):
7171

7272
@mock.patch('os.getpid')
7373
@mock.patch('os.fork')
74-
@mock.patch('os.execvpe')
75-
def test_arbiter_reexec_passing_systemd_sockets(execvpe, fork, getpid):
74+
@mock.patch('os.execve')
75+
@mock.patch('gunicorn.systemd.sd_notify')
76+
def test_arbiter_reexec_passing_systemd_sockets(sd_notify, execve, fork, getpid):
7677
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
7778
arbiter.LISTENERS = [mock.Mock(), mock.Mock()]
7879
arbiter.systemd = True
7980
fork.return_value = 0
81+
sd_notify.return_value = None
8082
getpid.side_effect = [2, 3]
8183
arbiter.reexec()
82-
environ = execvpe.call_args[0][2]
84+
environ = execve.call_args[0][2]
8385
assert environ['GUNICORN_PID'] == '2'
8486
assert environ['LISTEN_FDS'] == '2'
8587
assert environ['LISTEN_PID'] == '3'
88+
sd_notify.assert_called_once()
8689

8790

8891
@mock.patch('os.getpid')
8992
@mock.patch('os.fork')
90-
@mock.patch('os.execvpe')
91-
def test_arbiter_reexec_passing_gunicorn_sockets(execvpe, fork, getpid):
93+
@mock.patch('os.execve')
94+
def test_arbiter_reexec_passing_gunicorn_sockets(execve, fork, getpid):
9295
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
9396
listener1 = mock.Mock()
9497
listener2 = mock.Mock()
@@ -98,7 +101,7 @@ def test_arbiter_reexec_passing_gunicorn_sockets(execvpe, fork, getpid):
98101
fork.return_value = 0
99102
getpid.side_effect = [2, 3]
100103
arbiter.reexec()
101-
environ = execvpe.call_args[0][2]
104+
environ = execve.call_args[0][2]
102105
assert environ['GUNICORN_FD'] == '4,5'
103106
assert environ['GUNICORN_PID'] == '2'
104107

0 commit comments

Comments
 (0)