Skip to content

Commit

Permalink
Add child_exit() callback
Browse files Browse the repository at this point in the history
This is similar to worker_exit() in that it is called just after a
worker has terminated, but it's called in the Gunicorn *master* process,
not the *child* process.
  • Loading branch information
jonashaag authored and Jonas Haag committed Nov 25, 2016
1 parent b3c4260 commit 53fd1c1
Show file tree
Hide file tree
Showing 3 changed files with 35 additions and 3 deletions.
7 changes: 5 additions & 2 deletions gunicorn/arbiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,19 +498,22 @@ def reap_workers(self):
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."
raise HaltServer(reason, self.WORKER_BOOT_ERROR)
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
Expand Down
19 changes: 18 additions & 1 deletion gunicorn/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1634,6 +1634,23 @@ def post_request(worker, req, environ, resp):
"""


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"
Expand All @@ -1644,7 +1661,7 @@ def worker_exit(server, worker):
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.

This comment has been minimized.

Copy link
@rthille

rthille Feb 1, 2019

It seems that this is accurate, but incomplete. worker_exit is called both in the worker process, and in the parent when killing a worker.

This comment has been minimized.

Copy link
@tilgovi

tilgovi Feb 4, 2019

Collaborator

I am not sure it is supposed to. It could be an error. Line 644 of the arbiter, in kill_worker. I'm not sure we should be calling the hook there.

This comment has been minimized.

Copy link
@rthille

rthille Feb 4, 2019

Yeah, it'd definitely be clearer to have two separate hooks, one called in the worker/child and one in the master/parent.

The callable needs to accept two instance variables for the Arbiter and
the just-exited Worker.
Expand Down
12 changes: 12 additions & 0 deletions tests/test_arbiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 53fd1c1

Please sign in to comment.