From 68ac2e4bb2502e74efc9ac920d0de2b6ec2f57c3 Mon Sep 17 00:00:00 2001 From: Tanmoy Sarkar Date: Fri, 12 Jun 2026 23:23:18 +0530 Subject: [PATCH] fix(companion): Cancel pending restart when stopping a companion A stop issued while a restart was in flight (state STOPPING, restart_pending set) was ignored: handle_exit checked restart_pending first and respawned the companion the user had just stopped. Clear restart_pending in stop_process so manual stop wins. Co-Authored-By: Claude Opus 4.8 (1M context) --- gunicorn/companion/manager.py | 3 +++ tests/test_companion_manager.py | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/gunicorn/companion/manager.py b/gunicorn/companion/manager.py index 2a032758..58299006 100644 --- a/gunicorn/companion/manager.py +++ b/gunicorn/companion/manager.py @@ -419,6 +419,9 @@ class CompanionManager: if process is None: return False, "unknown companion %s" % name process.manual_stop = True + # A stop must win over an in-flight restart: clearing this keeps + # handle_exit from respawning a companion the user asked to stop. + process.restart_pending = False if process.state in (State.STOPPED, State.STOPPING): return True, "%s already %s" % (name, process.state.lower()) if process.state == State.BACKOFF: diff --git a/tests/test_companion_manager.py b/tests/test_companion_manager.py index 356882da..5be95619 100644 --- a/tests/test_companion_manager.py +++ b/tests/test_companion_manager.py @@ -420,6 +420,23 @@ def test_stop_process_unknown(): assert not ok +def test_stop_during_restart_cancels_pending_restart(): + manager = make_manager("rq") + proc = manager.processes["rq"] + proc.state = State.STOPPING + proc.pid = 70 + proc.restart_pending = True + with mock.patch("os.kill") as kill: + ok, _ = manager.stop_process("rq") + kill.assert_not_called() + assert ok and proc.manual_stop is True and proc.restart_pending is False + # On exit the companion now settles STOPPED instead of being respawned. + with mock.patch.object(manager, "spawn_process") as spawn: + manager.handle_exit(proc) + spawn.assert_not_called() + assert proc.state == State.STOPPED + + def test_signal_number_resolves_name(): assert CompanionManager._signal_number("SIGKILL") == signal.SIGKILL assert CompanionManager._signal_number(9) == 9