fix(gateway): don't restart supervised services on clean exit
The s6 finish script for profile-gateway services restarted on ANY exit except EX_CONFIG (78) — including clean exit 0. Restart-on-normal-exit turns an intentional stop into a reconnect loop: the ashriel-discord storm in #76435 made 1,000+ connections and got the bot token reset by Discord. The finish script now exits 125 (permanent failure, no restart) for both clean exit 0 and EX_CONFIG; only non-zero, non-78 exits (genuine crashes) restart normally. Scope note: #76435 bundles a second, separable symptom (Windows desktop updater showing the literal 'managed outside dashboard' sentinel). Its root cause is undiagnosed and #22733 covers the dialog-explanation path; this PR is the gateway half only. Tests: behavioral — the rendered finish script is executed via sh for exit codes 0/78/1/137, asserting no-restart for clean stop and fatal config, restart for crashes. Pre-existing EX_CONFIG test still passes.
This commit is contained in:
@@ -716,8 +716,12 @@ class S6ServiceManager:
|
||||
When the gateway exits with EX_CONFIG (78) — a fatal
|
||||
configuration error such as a token collision or no messaging
|
||||
platforms — we tell s6-supervise to stop restarting by exiting
|
||||
125 (permanent failure). Any other exit code lets s6 restart
|
||||
normally. See #51228.
|
||||
125 (permanent failure). A clean exit 0 is an intentional stop,
|
||||
not a crash: restarting after it turns any normal gateway exit
|
||||
into a reconnect loop (the ashriel-discord storm in #76435 —
|
||||
1,000+ connections and a provider token reset). Only non-zero,
|
||||
non-78 exits (genuine crashes) let s6 restart normally.
|
||||
See #51228, #76435.
|
||||
"""
|
||||
from gateway.restart import GATEWAY_FATAL_CONFIG_EXIT_CODE
|
||||
|
||||
@@ -727,9 +731,13 @@ class S6ServiceManager:
|
||||
"# shellcheck shell=sh\n"
|
||||
"# $1 = exit code from the run script.\n"
|
||||
f"# Exit {code} (EX_CONFIG) = fatal config error — don't restart.\n"
|
||||
"# Exit 0 (clean stop) = intentional stop — don't restart.\n"
|
||||
f'if [ "$1" = "{code}" ]; then\n'
|
||||
" exit 125\n"
|
||||
"fi\n"
|
||||
'if [ "$1" = "0" ]; then\n'
|
||||
" exit 125\n"
|
||||
"fi\n"
|
||||
"exit 0\n"
|
||||
)
|
||||
|
||||
|
||||
@@ -286,6 +286,28 @@ def test_render_finish_script_exits_125_on_ex_config() -> None:
|
||||
assert "exit 0" in text
|
||||
|
||||
|
||||
def test_render_finish_script_does_not_restart_on_clean_exit(tmp_path) -> None:
|
||||
"""Behavioral: the rendered finish script, executed for each run-exit
|
||||
code, must exit 125 (no restart) for clean exit 0 and EX_CONFIG 78,
|
||||
and exit 0 (restart) for genuine crashes (#76435 — restart-on-normal-
|
||||
exit turned a supervised gateway into a reconnect storm)."""
|
||||
import subprocess
|
||||
|
||||
script = tmp_path / "finish"
|
||||
script.write_text(S6ServiceManager._render_finish_script())
|
||||
script.chmod(0o755)
|
||||
|
||||
def finish_exit(run_exit_code: int) -> int:
|
||||
proc = subprocess.run(["sh", str(script), str(run_exit_code)],
|
||||
capture_output=True)
|
||||
return proc.returncode
|
||||
|
||||
assert finish_exit(0) == 125 # clean stop — no restart
|
||||
assert finish_exit(78) == 125 # fatal config — no restart
|
||||
assert finish_exit(1) == 0 # crash — s6 restarts
|
||||
assert finish_exit(137) == 0 # SIGKILL crash — s6 restarts
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user