fix(gateway): budget restart observer for full stop envelope

(cherry picked from commit e9b2123af68ce955dd66b027404687a0a375def3)
This commit is contained in:
Brian Le
2026-09-26 05:02:57 -07:00
committed by kshitij
parent 5aad192995
commit 643b2091f0
2 changed files with 22 additions and 10 deletions

View File

@@ -358,6 +358,9 @@ def resolve_systemd_timeout_stop_sec(
def resolve_restart_exit_wait_budget(
drain_timeout: float, after_turn_timeout: float, cron_drain_timeout: float = 0.0, *, headroom: float = 15.0,
) -> float:
"""Seconds a CLI should wait for the gateway PID to exit after SIGUSR1: in-band restart may
defer ``stop()`` until turns finish, then spend the longer chat or cron drain inside it."""
return max(_seconds(drain_timeout), _seconds(cron_drain_timeout)) + _seconds(after_turn_timeout) + _seconds(headroom)
"""Observer budget for in-band deferral, the full stop envelope, and replacement startup.
The stop envelope includes cron cleanup reserve, supervisor headroom and the floor;
deferral precedes it, while observer headroom follows it. Cron zero opts out.
"""
return _seconds(after_turn_timeout) + resolve_systemd_timeout_stop_sec(drain_timeout, cron_drain_timeout) + _seconds(headroom)

View File

@@ -4,6 +4,7 @@ from gateway.restart import (
DEFAULT_GATEWAY_RESTART_AFTER_TURN_TIMEOUT,
parse_restart_after_turn_timeout,
resolve_restart_exit_wait_budget,
resolve_systemd_timeout_stop_sec,
)
from gateway.run import GatewayRunner
@@ -20,12 +21,15 @@ def test_parse_restart_after_turn_timeout_defaults_and_clamps():
def test_resolve_restart_exit_wait_budget_covers_both_phases():
assert resolve_restart_exit_wait_budget(0, 0, headroom=15) == 15.0
assert resolve_restart_exit_wait_budget(180, 21600, headroom=15) == 180 + 21600 + 15
assert resolve_restart_exit_wait_budget(2, 3, 80, headroom=15) == 3 + 80 + 15
assert resolve_restart_exit_wait_budget(80, 3, 2, headroom=15) == 3 + 80 + 15
assert resolve_restart_exit_wait_budget(2, 3, 0, headroom=15) == 3 + 2 + 15
assert resolve_restart_exit_wait_budget("bad", "bad", headroom="x") == 0.0
assert resolve_restart_exit_wait_budget(0, 0, 0, headroom=15) == resolve_systemd_timeout_stop_sec(0, 0) + 15
assert resolve_restart_exit_wait_budget(180, 21600, 0, headroom=15) == 21600 + resolve_systemd_timeout_stop_sec(180, 0) + 15
for chat, cron in ((2, 80), (80, 2), (2, 0), (0, 0)):
stop_envelope = resolve_systemd_timeout_stop_sec(chat, cron)
assert resolve_restart_exit_wait_budget(chat, 3, cron, headroom=15) == 3 + stop_envelope + 15
# A bounded observer uses the longer stop path, not the sum of independent drains.
assert resolve_restart_exit_wait_budget(80, 3, 80, headroom=15) == 3 + resolve_systemd_timeout_stop_sec(80, 80) + 15
assert resolve_restart_exit_wait_budget(2, 3, 0, headroom=15) < resolve_restart_exit_wait_budget(2, 3, 80, headroom=15)
assert resolve_restart_exit_wait_budget("bad", "bad", 0, headroom="x") == 60.0
def test_cli_restart_wait_covers_configured_cron_drain(tmp_path, monkeypatch):
@@ -38,7 +42,12 @@ def test_cli_restart_wait_covers_configured_cron_drain(tmp_path, monkeypatch):
"agent:\n restart_drain_timeout: 2\n restart_after_turn_timeout: 3\n cron_drain_timeout: 80\n",
encoding="utf-8",
)
assert gateway_cli._get_restart_exit_wait_budget() >= 3 + 80 + 15
assert gateway_cli._get_restart_exit_wait_budget() == 3 + resolve_systemd_timeout_stop_sec(2, 80) + 15
(tmp_path / "config.yaml").write_text(
"agent:\n restart_drain_timeout: 2\n restart_after_turn_timeout: 3\n cron_drain_timeout: 0\n",
encoding="utf-8",
)
assert gateway_cli._get_restart_exit_wait_budget() == 3 + resolve_systemd_timeout_stop_sec(2, 0) + 15
def test_load_restart_after_turn_timeout_preserves_zero(tmp_path, monkeypatch):