diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index ff5d6988..3109b765 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -498,8 +498,9 @@ class Arbiter(object): if self.reexec_pid == wpid: self.reexec_pid = 0 else: - # A worker said it cannot boot. We'll shutdown - # to avoid infinite start/stop cycles. + # A worker was terminated. If the termination reason was + # that it could not boot, we'll shut it down to avoid + # infinite start/stop cycles. exitcode = status >> 8 if exitcode == self.WORKER_BOOT_ERROR: reason = "Worker failed to boot." @@ -507,10 +508,12 @@ class Arbiter(object): if exitcode == self.APP_LOAD_ERROR: reason = "App failed to load." raise HaltServer(reason, self.APP_LOAD_ERROR) + worker = self.WORKERS.pop(wpid, None) if not worker: continue worker.tmp.close() + self.cfg.child_exit(self, worker) except OSError as e: if e.errno != errno.ECHILD: raise diff --git a/gunicorn/config.py b/gunicorn/config.py index 8369d974..b1b6d81d 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -1634,6 +1634,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): name = "worker_exit" section = "Server Hooks" @@ -1644,7 +1661,7 @@ class WorkerExit(Setting): pass default = staticmethod(worker_exit) 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 just-exited Worker. diff --git a/tests/test_arbiter.py b/tests/test_arbiter.py index 213273c0..eda56f52 100644 --- a/tests/test_arbiter.py +++ b/tests/test_arbiter.py @@ -55,6 +55,18 @@ def test_arbiter_calls_worker_exit(mock_os_fork): 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): """ Simple application that makes use of the 'preload' feature to