Skip to content

Commit 76f761f

Browse files
committed
systemd: $MAINPID handover for NotifyAccess=main
This reverts dup-ing the socket fds on re-exec (and --bind=fd://3, and systemd socket activation)
1 parent bc9b147 commit 76f761f

File tree

5 files changed

+45
-8
lines changed

5 files changed

+45
-8
lines changed

docs/source/settings.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,6 +1595,11 @@ If the ``PORT`` environment variable is defined, the default
15951595
is ``['0.0.0.0:$PORT']``. If it is not defined, the default
15961596
is ``['127.0.0.1:8000']``.
15971597

1598+
.. note::
1599+
Specifying any fd://FD socket or inheriting any socket from systemd
1600+
(LISTEN_FDS) results in other bind addresses to be skipped.
1601+
Do not mix fd://FD and systemd socket activation.
1602+
15981603
.. _backlog:
15991604

16001605
``backlog``

gunicorn/arbiter.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ def start(self):
151151
self.systemd = True
152152
fds = range(systemd.SD_LISTEN_FDS_START,
153153
systemd.SD_LISTEN_FDS_START + listen_fds)
154+
self.log.debug("Inherited sockets from systemd: %r", fds)
154155

155156
elif self.master_pid:
156157
fds = []
@@ -172,6 +173,10 @@ def start(self):
172173

173174
self.cfg.when_ready(self)
174175

176+
# # call `pkill --oldest -TERM -f "gunicorn: master "` instead
177+
# if self.master_pid and self.systemd:
178+
# os.kill(self.master_pid, signal.SIGTERM)
179+
175180
def init_signals(self):
176181
"""\
177182
Initialize master signal handling. Most of the signals
@@ -350,7 +355,12 @@ def wakeup(self):
350355

351356
def halt(self, reason=None, exit_status=0):
352357
""" halt arbiter """
353-
systemd.sd_notify("STOPPING=1\nSTATUS=Gunicorn shutting down..\n", self.log)
358+
if self.master_pid != 0:
359+
# if NotifyAccess=main, systemd needs to know old master is in control
360+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=New arbiter shutdown\n" % (self.master_pid, ), self.log)
361+
elif self.reexec_pid == 0:
362+
# skip setting status if this is merely superseded master stopping
363+
systemd.sd_notify("STOPPING=1\nSTATUS=Shutting down..\n", self.log)
354364

355365
self.stop()
356366

@@ -425,6 +435,10 @@ def reexec(self):
425435
master_pid = os.getpid()
426436
self.reexec_pid = os.fork()
427437
if self.reexec_pid != 0:
438+
# let systemd know they will be in control after exec()
439+
systemd.sd_notify(
440+
"RELOADING=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec in forked..\n" % (self.reexec_pid, ), self.log
441+
)
428442
# old master
429443
return
430444

@@ -437,6 +451,9 @@ def reexec(self):
437451
if self.systemd:
438452
environ['LISTEN_PID'] = str(os.getpid())
439453
environ['LISTEN_FDS'] = str(len(self.LISTENERS))
454+
# move socket fds back to 3+N after we duped+closed them
455+
# for idx, lnr in enumerate(self.LISTENERS):
456+
# os.dup2(lnr.fileno(), 3+idx)
440457
else:
441458
environ['GUNICORN_FD'] = ','.join(
442459
str(lnr.fileno()) for lnr in self.LISTENERS)
@@ -445,8 +462,10 @@ def reexec(self):
445462

446463
# exec the process using the original environment
447464
self.log.debug("exe=%r argv=%r" % (self.START_CTX[0], self.START_CTX['args']))
448-
# let systemd know are are in control
449-
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec\n" % (master_pid, ), self.log)
465+
# let systemd know we will be in control after exec()
466+
systemd.sd_notify(
467+
"RELOADING=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec in progress..\n" % (self.reexec_pid, ), self.log
468+
)
450469
os.execve(self.START_CTX[0], self.START_CTX['args'], environ)
451470

452471
def reload(self):
@@ -538,8 +557,7 @@ def reap_workers(self):
538557
self.reexec_pid = 0
539558
self.log.info("Master exited before promotion.")
540559
# let systemd know we are (back) in control
541-
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec aborted\n" % (os.getpid(), ), self.log)
542-
continue
560+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Old arbiter promoted\n" % (os.getpid(), ), self.log)
543561
else:
544562
worker = self.WORKERS.pop(wpid, None)
545563
if not worker:

gunicorn/config.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,11 @@ class Bind(Setting):
616616
If the ``PORT`` environment variable is defined, the default
617617
is ``['0.0.0.0:$PORT']``. If it is not defined, the default
618618
is ``['127.0.0.1:8000']``.
619+
620+
.. note::
621+
Specifying any fd://FD socket or inheriting any socket from systemd
622+
(LISTEN_FDS) results in other bind addresses to be skipped.
623+
Do not mix fd://FD and systemd socket activation.
619624
"""
620625

621626

gunicorn/instrument/statsd.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def __init__(self, cfg):
3535
self.sock = socket.socket(address_family, socket.SOCK_DGRAM)
3636
self.sock.connect(cfg.statsd_host)
3737
except Exception:
38+
self.sock.close()
3839
self.sock = None
3940

4041
self.dogstatsd_tags = cfg.dogstatsd_tags

gunicorn/sock.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def __init__(self, address, conf, log, fd=None):
2424
sock = socket.socket(self.FAMILY, socket.SOCK_STREAM)
2525
bound = False
2626
else:
27-
sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM)
28-
os.close(fd)
27+
# does not duplicate the fd, this LISTEN_FDS stays at fds 3+N
28+
sock = socket.socket(self.FAMILY, socket.SOCK_STREAM, fileno=fd)
2929
bound = True
3030

3131
self.sock = self.set_options(sock, bound=bound)
@@ -156,6 +156,12 @@ def create_sockets(conf, log, fds=None):
156156
fdaddr += list(fds)
157157
laddr = [bind for bind in addr if not isinstance(bind, int)]
158158

159+
# LISTEN_FDS=1 + fd://3
160+
uniq_fdaddr = set()
161+
duped_fdaddr = {fd for fd in fdaddr if fd in uniq_fdaddr or uniq_fdaddr.add(fd)}
162+
if duped_fdaddr:
163+
log.warning("Binding with fd:// is unsupported with systemd/re-exec.")
164+
159165
# check ssl config early to raise the error on startup
160166
# only the certfile is needed since it can contains the keyfile
161167
if conf.certfile and not os.path.exists(conf.certfile):
@@ -167,9 +173,11 @@ def create_sockets(conf, log, fds=None):
167173
# sockets are already bound
168174
if fdaddr:
169175
for fd in fdaddr:
170-
sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM)
176+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, fileno=fd)
171177
sock_name = sock.getsockname()
172178
sock_type = _sock_type(sock_name)
179+
log.debug("listen: fd %d => fd %d for %s", fd, sock.fileno(), sock.getsockname())
180+
sock.detach() # only created to call getsockname(), will re-attach shorty
173181
listener = sock_type(sock_name, conf, log, fd=fd)
174182
listeners.append(listener)
175183

0 commit comments

Comments
 (0)