From 9f0e64ceddb87fc16c287f0898a080b58988905f Mon Sep 17 00:00:00 2001 From: LeonSGP43 Date: Fri, 26 Jun 2026 23:13:35 +0800 Subject: [PATCH] fix(gateway): force exit after graceful shutdown Co-Authored-By: Paperclip --- gateway/run.py | 18 +++++-- tests/gateway/test_gateway_process_exit.py | 58 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 tests/gateway/test_gateway_process_exit.py diff --git a/gateway/run.py b/gateway/run.py index 1cd2c9c053a..0289afb3c4a 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -18689,11 +18689,21 @@ def main(): data = yaml.safe_load(f) or {} config = GatewayConfig.from_dict(data) - # Run the gateway - exit with code 1 if no platforms connected, - # so systemd Restart=on-failure will retry on transient errors (e.g. DNS) + # start_gateway() already performs graceful teardown before returning. + # Force-exit afterwards so a wedged non-daemon worker thread cannot block + # interpreter finalization and strand the gateway half-shut down. success = asyncio.run(start_gateway(config)) - if not success: - sys.exit(1) + _exit_after_graceful_shutdown(success) + + +def _exit_after_graceful_shutdown(success: bool) -> None: + """Flush stdio and terminate immediately after graceful shutdown.""" + for stream in (sys.stdout, sys.stderr): + try: + stream.flush() + except Exception: + pass + os._exit(0 if success else 1) if __name__ == "__main__": diff --git a/tests/gateway/test_gateway_process_exit.py b/tests/gateway/test_gateway_process_exit.py new file mode 100644 index 00000000000..de42cbbfb5f --- /dev/null +++ b/tests/gateway/test_gateway_process_exit.py @@ -0,0 +1,58 @@ +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +import gateway.run as gateway_run + + +class _ExitCalled(Exception): + def __init__(self, code: int): + super().__init__(code) + self.code = code + + +def _raise_exit(code: int) -> None: + raise _ExitCalled(code) + + +def test_main_force_exits_zero_after_clean_shutdown(monkeypatch): + async def fake_start_gateway(config=None): + return True + + stdout = SimpleNamespace(flush=Mock()) + stderr = SimpleNamespace(flush=Mock()) + + monkeypatch.setattr(gateway_run, "start_gateway", fake_start_gateway) + monkeypatch.setattr(gateway_run.os, "_exit", _raise_exit) + monkeypatch.setattr(gateway_run.sys, "argv", ["gateway.run"]) + monkeypatch.setattr(gateway_run.sys, "stdout", stdout) + monkeypatch.setattr(gateway_run.sys, "stderr", stderr) + + with pytest.raises(_ExitCalled) as exc_info: + gateway_run.main() + + assert exc_info.value.code == 0 + stdout.flush.assert_called_once_with() + stderr.flush.assert_called_once_with() + + +def test_main_force_exits_one_after_failed_shutdown(monkeypatch): + async def fake_start_gateway(config=None): + return False + + stdout = SimpleNamespace(flush=Mock()) + stderr = SimpleNamespace(flush=Mock()) + + monkeypatch.setattr(gateway_run, "start_gateway", fake_start_gateway) + monkeypatch.setattr(gateway_run.os, "_exit", _raise_exit) + monkeypatch.setattr(gateway_run.sys, "argv", ["gateway.run"]) + monkeypatch.setattr(gateway_run.sys, "stdout", stdout) + monkeypatch.setattr(gateway_run.sys, "stderr", stderr) + + with pytest.raises(_ExitCalled) as exc_info: + gateway_run.main() + + assert exc_info.value.code == 1 + stdout.flush.assert_called_once_with() + stderr.flush.assert_called_once_with()