Skip to content

Commit 86ed0ca

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 ff883b4 commit 86ed0ca

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
@@ -150,6 +150,7 @@ def start(self):
150150
self.systemd = True
151151
fds = range(systemd.SD_LISTEN_FDS_START,
152152
systemd.SD_LISTEN_FDS_START + listen_fds)
153+
self.log.debug("Inherited sockets from systemd: %r", fds)
153154

154155
elif self.master_pid:
155156
fds = []
@@ -170,6 +171,10 @@ def start(self):
170171

171172
self.cfg.when_ready(self)
172173

174+
# # call `pkill --oldest -TERM -f "gunicorn: master "` instead
175+
# if self.master_pid and self.systemd:
176+
# os.kill(self.master_pid, signal.SIGTERM)
177+
173178
def init_signals(self):
174179
"""\
175180
Initialize master signal handling. Most of the signals
@@ -348,7 +353,12 @@ def wakeup(self):
348353

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

353363
self.stop()
354364

@@ -423,6 +433,10 @@ def reexec(self):
423433
master_pid = os.getpid()
424434
self.reexec_pid = os.fork()
425435
if self.reexec_pid != 0:
436+
# let systemd know they will be in control after exec()
437+
systemd.sd_notify(
438+
"RELOADING=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec in forked..\n" % (self.reexec_pid, ), self.log
439+
)
426440
# old master
427441
return
428442

@@ -435,6 +449,9 @@ def reexec(self):
435449
if self.systemd:
436450
environ['LISTEN_PID'] = str(os.getpid())
437451
environ['LISTEN_FDS'] = str(len(self.LISTENERS))
452+
# move socket fds back to 3+N after we duped+closed them
453+
# for idx, lnr in enumerate(self.LISTENERS):
454+
# os.dup2(lnr.fileno(), 3+idx)
438455
else:
439456
environ['GUNICORN_FD'] = ','.join(
440457
str(lnr.fileno()) for lnr in self.LISTENERS)
@@ -443,8 +460,10 @@ def reexec(self):
443460

444461
# exec the process using the original environment
445462
self.log.debug("exe=%r argv=%r" % (self.START_CTX[0], self.START_CTX['args']))
446-
# let systemd know are are in control
447-
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec\n" % (master_pid, ), self.log)
463+
# let systemd know we will be in control after exec()
464+
systemd.sd_notify(
465+
"RELOADING=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec in progress..\n" % (self.reexec_pid, ), self.log
466+
)
448467
os.execve(self.START_CTX[0], self.START_CTX['args'], environ)
449468

450469
def reload(self):
@@ -536,8 +555,7 @@ def reap_workers(self):
536555
self.reexec_pid = 0
537556
self.log.info("Master exited before promotion.")
538557
# let systemd know we are (back) in control
539-
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Gunicorn arbiter re-exec aborted\n" % (os.getpid(), ), self.log)
540-
continue
558+
systemd.sd_notify("READY=1\nMAINPID=%d\nSTATUS=Old arbiter promoted\n" % (os.getpid(), ), self.log)
541559
else:
542560
worker = self.WORKERS.pop(wpid, None)
543561
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)