From 403af92504174bae6adda292458a3f730914e7ea Mon Sep 17 00:00:00 2001 From: LeonSGP43 Date: Wed, 27 May 2026 11:18:01 +0800 Subject: [PATCH] fix(gateway): avoid restart loops on replace --- hermes_cli/gateway.py | 14 ++++++++------ tests/hermes_cli/test_gateway_service.py | 4 ++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index 867319574806..6d59fe127e29 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -207,9 +207,11 @@ def _graceful_restart_via_sigusr1(pid: int, drain_timeout: float) -> bool: SIGUSR1 is wired in gateway/run.py to ``request_restart(via_service=True)`` which drains in-flight agent runs (up to ``agent.restart_drain_timeout`` - seconds), then exits with code 75. Both systemd (``Restart=always`` - + ``RestartForceExitStatus=75``) and launchd (``KeepAlive.SuccessfulExit - = false``) relaunch the process after the graceful exit. + seconds), then exits with code 75. Systemd units generated by Hermes use + ``Restart=on-failure`` together with ``RestartForceExitStatus=75`` so the + service is relaunched after the graceful exit without reviving clean + ``--replace`` takeovers. launchd still uses ``KeepAlive.SuccessfulExit = + false`` for the same relaunch behavior. This is the drain-aware alternative to ``systemctl restart`` / ``SIGTERM``, which SIGKILL in-flight agents after a short timeout. @@ -2217,7 +2219,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) Environment="PATH={sane_path}" Environment="VIRTUAL_ENV={venv_dir}" Environment="HERMES_HOME={hermes_home}" -Restart=always +Restart=on-failure RestartSec=5 RestartMaxDelaySec=300 RestartSteps=5 @@ -2252,7 +2254,7 @@ def generate_systemd_unit(system: bool = False, run_as_user: str | None = None) Environment="PATH={sane_path}" Environment="VIRTUAL_ENV={venv_dir}" Environment="HERMES_HOME={hermes_home}" -Restart=always +Restart=on-failure RestartSec=5 RestartMaxDelaySec=300 RestartSteps=5 @@ -3236,7 +3238,7 @@ def run_gateway(verbose: int = 0, quiet: bool = False, replace: bool = False): print() # Exit with code 1 if gateway fails to connect any platform, - # so systemd Restart=always will retry on transient errors + # so systemd Restart=on-failure will retry on transient errors verbosity = None if quiet else verbose # ── Exit-path diagnostics ──────────────────────────────────────────── diff --git a/tests/hermes_cli/test_gateway_service.py b/tests/hermes_cli/test_gateway_service.py index b1fcadbf4f0d..55540fb8f5dd 100644 --- a/tests/hermes_cli/test_gateway_service.py +++ b/tests/hermes_cli/test_gateway_service.py @@ -326,6 +326,8 @@ def test_user_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(self assert "ExecStart=" in unit assert "ExecStop=" not in unit assert "ExecReload=/bin/kill -USR1 $MAINPID" in unit + assert "Restart=on-failure" in unit + assert "Restart=always" not in unit assert f"RestartForceExitStatus={GATEWAY_SERVICE_RESTART_EXIT_CODE}" in unit # TimeoutStopSec must exceed the default drain_timeout (60s) so # systemd doesn't SIGKILL the cgroup before post-interrupt cleanup @@ -387,6 +389,8 @@ def test_system_unit_avoids_recursive_execstop_and_uses_extended_stop_timeout(se assert "ExecStart=" in unit assert "ExecStop=" not in unit assert "ExecReload=/bin/kill -USR1 $MAINPID" in unit + assert "Restart=on-failure" in unit + assert "Restart=always" not in unit assert f"RestartForceExitStatus={GATEWAY_SERVICE_RESTART_EXIT_CODE}" in unit # TimeoutStopSec must exceed the default drain_timeout (60s) so # systemd doesn't SIGKILL the cgroup before post-interrupt cleanup