Merge pull request #1394 from jonashaag/master

Add child_exit() callback to configuration
This commit is contained in:
Benoit Chesneau 2017-01-27 16:00:58 +01:00 committed by GitHub
commit 21ffa92be1
4 changed files with 57 additions and 11 deletions

View File

@ -515,8 +515,9 @@ class Arbiter(object):
if self.reexec_pid == wpid: if self.reexec_pid == wpid:
self.reexec_pid = 0 self.reexec_pid = 0
else: else:
# A worker said it cannot boot. We'll shutdown # A worker was terminated. If the termination reason was
# to avoid infinite start/stop cycles. # that it could not boot, we'll shut it down to avoid
# infinite start/stop cycles.
exitcode = status >> 8 exitcode = status >> 8
if exitcode == self.WORKER_BOOT_ERROR: if exitcode == self.WORKER_BOOT_ERROR:
reason = "Worker failed to boot." reason = "Worker failed to boot."
@ -524,10 +525,12 @@ class Arbiter(object):
if exitcode == self.APP_LOAD_ERROR: if exitcode == self.APP_LOAD_ERROR:
reason = "App failed to load." reason = "App failed to load."
raise HaltServer(reason, self.APP_LOAD_ERROR) raise HaltServer(reason, self.APP_LOAD_ERROR)
worker = self.WORKERS.pop(wpid, None) worker = self.WORKERS.pop(wpid, None)
if not worker: if not worker:
continue continue
worker.tmp.close() worker.tmp.close()
self.cfg.child_exit(self, worker)
except OSError as e: except OSError as e:
if e.errno != errno.ECHILD: if e.errno != errno.ECHILD:
raise raise
@ -562,14 +565,15 @@ class Arbiter(object):
self.cfg.pre_fork(self, worker) self.cfg.pre_fork(self, worker)
pid = os.fork() pid = os.fork()
if pid != 0: if pid != 0:
worker.pid = pid
self.WORKERS[pid] = worker self.WORKERS[pid] = worker
return pid return pid
# Process Child # Process Child
worker_pid = os.getpid() worker.pid = os.getpid()
try: try:
util._setproctitle("worker [%s]" % self.proc_name) util._setproctitle("worker [%s]" % self.proc_name)
self.log.info("Booting worker with pid: %s", worker_pid) self.log.info("Booting worker with pid: %s", worker.pid)
self.cfg.post_fork(self, worker) self.cfg.post_fork(self, worker)
worker.init_process() worker.init_process()
sys.exit(0) sys.exit(0)
@ -587,7 +591,7 @@ class Arbiter(object):
sys.exit(self.WORKER_BOOT_ERROR) sys.exit(self.WORKER_BOOT_ERROR)
sys.exit(-1) sys.exit(-1)
finally: finally:
self.log.info("Worker exiting (pid: %s)", worker_pid) self.log.info("Worker exiting (pid: %s)", worker.pid)
try: try:
worker.tmp.close() worker.tmp.close()
self.cfg.worker_exit(self, worker) self.cfg.worker_exit(self, worker)

View File

@ -1650,6 +1650,23 @@ class PostRequest(Setting):
""" """
class ChildExit(Setting):
name = "child_exit"
section = "Server Hooks"
validator = validate_callable(2)
type = six.callable
def child_exit(server, worker):
pass
default = staticmethod(child_exit)
desc = """\
Called just after a worker has been exited, in the master process.
The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker.
"""
class WorkerExit(Setting): class WorkerExit(Setting):
name = "worker_exit" name = "worker_exit"
section = "Server Hooks" section = "Server Hooks"
@ -1660,7 +1677,7 @@ class WorkerExit(Setting):
pass pass
default = staticmethod(worker_exit) default = staticmethod(worker_exit)
desc = """\ desc = """\
Called just after a worker has been exited. Called just after a worker has been exited, in the worker process.
The callable needs to accept two instance variables for the Arbiter and The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker. the just-exited Worker.

View File

@ -39,6 +39,7 @@ class Worker(object):
changes you'll want to do that in ``self.init_process()``. changes you'll want to do that in ``self.init_process()``.
""" """
self.age = age self.age = age
self.pid = "[booting]"
self.ppid = ppid self.ppid = ppid
self.sockets = sockets self.sockets = sockets
self.app = app self.app = app
@ -58,10 +59,6 @@ class Worker(object):
def __str__(self): def __str__(self):
return "<Worker %s>" % self.pid return "<Worker %s>" % self.pid
@property
def pid(self):
return os.getpid()
def notify(self): def notify(self):
"""\ """\
Your worker subclass must arrange to have this method called Your worker subclass must arrange to have this method called

View File

@ -16,7 +16,7 @@ import gunicorn.arbiter
class DummyApplication(gunicorn.app.base.BaseApplication): class DummyApplication(gunicorn.app.base.BaseApplication):
""" """
Dummy application that has an default configuration. Dummy application that has a default configuration.
""" """
def init(self, parser, opts, args): def init(self, parser, opts, args):
@ -114,6 +114,34 @@ def test_arbiter_reexec_limit_child(fork):
assert fork.called is False, "should not fork when arbiter is a child" assert fork.called is False, "should not fork when arbiter is a child"
@mock.patch('os.fork')
def test_arbiter_calls_worker_exit(mock_os_fork):
mock_os_fork.return_value = 0
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
arbiter.cfg.settings['worker_exit'] = mock.Mock()
arbiter.pid = None
mock_worker = mock.Mock()
arbiter.worker_class = mock.Mock(return_value=mock_worker)
try:
arbiter.spawn_worker()
except SystemExit:
pass
arbiter.cfg.worker_exit.assert_called_with(arbiter, mock_worker)
@mock.patch('os.waitpid')
def test_arbiter_reap_workers(mock_os_waitpid):
mock_os_waitpid.side_effect = [(42, 0), (0, 0)]
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
arbiter.cfg.settings['child_exit'] = mock.Mock()
mock_worker = mock.Mock()
arbiter.WORKERS = {42: mock_worker}
arbiter.reap_workers()
mock_worker.tmp.close.assert_called_with()
arbiter.cfg.child_exit.assert_called_with(arbiter, mock_worker)
class PreloadedAppWithEnvSettings(DummyApplication): class PreloadedAppWithEnvSettings(DummyApplication):
""" """
Simple application that makes use of the 'preload' feature to Simple application that makes use of the 'preload' feature to