fix(companion): Correct manager pid and reset companion signals

This commit is contained in:
Tanmoy Sarkar 2026-06-09 23:18:54 +05:30
parent 1827667cb2
commit a90eba0c17
2 changed files with 42 additions and 1 deletions

View File

@ -25,6 +25,28 @@ if TYPE_CHECKING:
PR_SET_PDEATHSIG = 1
# Signals the arbiter and manager install handlers for; a forked companion
# resets them to the default so its stop signal works and the target starts
# from a clean slate, the same way a gunicorn worker does.
INHERITED_SIGNALS = [
"SIGCHLD", "SIGTERM", "SIGINT", "SIGHUP", "SIGQUIT",
"SIGUSR1", "SIGUSR2", "SIGWINCH", "SIGTTIN", "SIGTTOU",
]
def reset_child_signals() -> None:
"""Restore default signal handling in a freshly forked companion.
The companion inherits the manager's (and arbiter's) handlers across the
fork. Without this, a stop signal like SIGTERM would hit the manager's
handler -- which just flips a flag -- instead of terminating the companion.
"""
for name in INHERITED_SIGNALS:
number = getattr(signal, name, None)
if number is not None:
signal.signal(number, signal.SIG_DFL)
def set_parent_death_signal(stop_signal) -> bool:
"""Ask the kernel to send ``stop_signal`` when this process's parent dies.
@ -79,6 +101,10 @@ class CompanionManager:
signal on Linux and, as a portable fallback, watches ``getppid`` each
tick so it never keeps companions running under a dead arbiter.
"""
# __init__ ran in the arbiter, so refresh pid/parent now that this is
# the manager process: the companion parent-death guard compares its
# getppid() against self.pid.
self.pid = os.getpid()
self.parent_pid = os.getppid()
self._install_signals()
set_parent_death_signal(signal.SIGTERM)
@ -325,6 +351,7 @@ class CompanionManager:
try:
self._close_manager_fds()
reset_child_signals()
set_parent_death_signal(signal.SIGTERM)
if os.getppid() != self.pid:
# Manager already died between fork and arming: do not run.

View File

@ -9,7 +9,11 @@ from unittest import mock
import pytest
from gunicorn.companion.control import CommandError
from gunicorn.companion.manager import CompanionManager, set_parent_death_signal
from gunicorn.companion.manager import (
CompanionManager,
reset_child_signals,
set_parent_death_signal,
)
from gunicorn.companion.config import CompanionConfig
from gunicorn.companion.process import State
@ -52,6 +56,16 @@ def test_log_exit_reports_signal_or_status():
assert any("status" in message for message in messages)
def test_reset_child_signals_restores_defaults():
with mock.patch("signal.signal") as signal_signal:
reset_child_signals()
restored = {call.args[0]: call.args[1] for call in signal_signal.call_args_list}
# The stop signal must reach the default disposition, not the manager's
# inherited handler, so a forked companion actually terminates on SIGTERM.
assert restored[signal.SIGTERM] is signal.SIG_DFL
assert restored[signal.SIGINT] is signal.SIG_DFL
def test_set_parent_death_signal_noop_off_linux():
with mock.patch("sys.platform", "darwin"):
assert set_parent_death_signal(signal.SIGTERM) is False