diff --git a/gunicorn/arbiter.py b/gunicorn/arbiter.py index e5217266..80989ebe 100644 --- a/gunicorn/arbiter.py +++ b/gunicorn/arbiter.py @@ -515,8 +515,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." @@ -524,10 +525,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 @@ -562,14 +565,15 @@ class Arbiter(object): self.cfg.pre_fork(self, worker) pid = os.fork() if pid != 0: + worker.pid = pid self.WORKERS[pid] = worker return pid # Process Child - worker_pid = os.getpid() + worker.pid = os.getpid() try: 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) worker.init_process() sys.exit(0) @@ -587,7 +591,7 @@ class Arbiter(object): sys.exit(self.WORKER_BOOT_ERROR) sys.exit(-1) finally: - self.log.info("Worker exiting (pid: %s)", worker_pid) + self.log.info("Worker exiting (pid: %s)", worker.pid) try: worker.tmp.close() self.cfg.worker_exit(self, worker) diff --git a/gunicorn/config.py b/gunicorn/config.py index 442f737c..37eae21e 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -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): name = "worker_exit" section = "Server Hooks" @@ -1660,7 +1677,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/gunicorn/workers/base.py b/gunicorn/workers/base.py index b4f24e6a..aeec3a2e 100644 --- a/gunicorn/workers/base.py +++ b/gunicorn/workers/base.py @@ -39,6 +39,7 @@ class Worker(object): changes you'll want to do that in ``self.init_process()``. """ self.age = age + self.pid = "[booting]" self.ppid = ppid self.sockets = sockets self.app = app @@ -58,10 +59,6 @@ class Worker(object): def __str__(self): return "" % self.pid - @property - def pid(self): - return os.getpid() - def notify(self): """\ Your worker subclass must arrange to have this method called diff --git a/tests/test_arbiter.py b/tests/test_arbiter.py index 5d22473e..5ab6e920 100644 --- a/tests/test_arbiter.py +++ b/tests/test_arbiter.py @@ -16,7 +16,7 @@ import gunicorn.arbiter 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): @@ -114,6 +114,34 @@ def test_arbiter_reexec_limit_child(fork): 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): """ Simple application that makes use of the 'preload' feature to