feat(companion): Close Gunicorn-only fds in the manager child

The forked companion manager inherits the arbiter's HTTP listening sockets,
its wakeup pipe, and the worker heartbeat files, none of which the manager
uses. Close them in the child before running so the manager and the companions
it forks do not pin the arbiter's fds. The manager creates its own signal pipe
and control socket after the fork.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tanmoy Sarkar 2026-06-09 22:50:52 +05:30
parent 22431f24e6
commit 31e08aac58
3 changed files with 34 additions and 1 deletions

View File

@ -691,7 +691,7 @@ No per-companion logic in Arbiter.
- [x] Add manager spawn/reap logic in Arbiter.
- [x] Add manager shutdown handling in Arbiter.
- [x] Wire Gunicorn reload to manager `reread` or restart.
- [ ] Close Gunicorn-only fds in manager child.
- [x] Close Gunicorn-only fds in manager child.
- [ ] Close manager-only fds in companion child.
- [ ] Add parent-death cleanup.
- [ ] Add lifecycle logs.

View File

@ -699,6 +699,7 @@ class Arbiter:
# Process Child
try:
self._close_gunicorn_fds()
util._setproctitle("companion manager [%s]" % self.proc_name)
manager.run()
sys.exit(0)
@ -708,6 +709,24 @@ class Arbiter:
self.log.exception("Exception in companion manager process")
sys.exit(-1)
def _close_gunicorn_fds(self):
"""Close fds the manager inherited from the arbiter but never uses.
The companion manager serves no HTTP traffic and does not run the
arbiter's signal loop, so it drops the listening sockets, the arbiter's
wakeup pipe, and the worker heartbeat files. Closing them keeps the
manager (and the companions it forks) from pinning the arbiter's fds.
"""
for listener in self.LISTENERS:
listener.close()
for pipe_fd in self.PIPE:
try:
os.close(pipe_fd)
except OSError:
pass
for worker in self.WORKERS.values():
worker.tmp.close()
def reload_companion_manager(self):
"""Restart the companion manager so it picks up the new configuration.

View File

@ -205,6 +205,20 @@ def test_stop_companion_manager_clears_pid_when_already_gone():
assert arbiter.companion_manager_pid == 0
def test_close_gunicorn_fds_in_manager_child():
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
listener = mock.Mock()
worker = mock.Mock()
arbiter.LISTENERS = [listener]
arbiter.WORKERS = {1: worker}
arbiter.PIPE = [7, 8]
with mock.patch("os.close") as os_close:
arbiter._close_gunicorn_fds()
listener.close.assert_called_once_with()
worker.tmp.close.assert_called_once_with()
assert os_close.call_count == 2
def test_reload_companion_manager_restarts_running():
arbiter = gunicorn.arbiter.Arbiter(DummyApplication())
arbiter.cfg.set("companion_workers", [{"name": "rq", "target": "pkg:run"}])