fix(gateway): log the converge hint on a standalone owner; put a refusal in the profile's own log
Two gaps left by the standalone-owner START (field report on #118097): * decide() now logs ONE WARNING when it starts beside another profile's standalone gateway, naming `hermes gateway migrate --multiplex`. The legacy per-profile topology stays live, but the fleet should be able to find out it is still on it from its own logs. * A REFUSE on `gateway run` prints to stdout, which under launchd is the unit's stdout file; the wrapper then maps 78 to 0 and the unit is parked with nothing in that profile's gateway/errors log. Log the verdict and the remedy at WARNING before exiting. The exit code is unchanged: a multiplexing owner that excludes the profile is a config-derived, permanent refusal, and 75 would make launchd relaunch it every ThrottleInterval forever (#89477) — the smaller correct change is to stop the refusal being silent, not to make it retry.
This commit is contained in:
@@ -312,6 +312,10 @@ def decide(our_home: Path, *, replace: bool = False) -> HostAttachDecision:
|
||||
# here exits 78, which every supervisor treats as permanent — on a launchd fleet that parked
|
||||
# every unit but the first to claim the host lock. Start beside it; the host-lock claim logs
|
||||
# the topology and the `gateway migrate --multiplex` path stays the way to converge.
|
||||
logger.warning(
|
||||
"Another profile's standalone gateway owns this host (%s); starting profile '%s' beside it. "
|
||||
"Fold every profile onto one gateway with: hermes gateway migrate --multiplex",
|
||||
attached.describe(), profile)
|
||||
return HostAttachDecision(START, "")
|
||||
if not gateway.served_known:
|
||||
# The owner never answered, so we know only that it exists. ATTACH here (on the record's
|
||||
|
||||
@@ -3797,7 +3797,12 @@ def _attach_to_host_gateway_or_guard(force: bool = False, replace: bool = False)
|
||||
if decision is not None and decision.outcome in (ATTACH, REFUSE):
|
||||
print(decision.message)
|
||||
if decision.outcome == REFUSE:
|
||||
sys.exit(_host_decision_exit_code(decision))
|
||||
code = _host_decision_exit_code(decision)
|
||||
# stdout goes to the supervisor's unit log; under launchd a permanent refusal is then
|
||||
# mapped to a clean exit and the unit is parked. The profile's own logs (errors.log,
|
||||
# WARNING+) are where a parked fleet is diagnosed, so name the verdict and the remedy there.
|
||||
logger.warning("gateway run refused (exit %d): %s", code, decision.message)
|
||||
sys.exit(code)
|
||||
if _running_under_gateway_supervisor():
|
||||
sys.exit(_host_decision_exit_code(decision))
|
||||
sys.exit(0)
|
||||
|
||||
@@ -148,7 +148,7 @@ def test_host_gateway_refuses_when_it_will_not_serve_the_profile(tmp_path, monke
|
||||
assert asyncio.run(gateway_run._host_attach_or_none(replace=False)) is False
|
||||
|
||||
|
||||
def test_a_standalone_owner_is_the_per_profile_topology_not_a_refusal(tmp_path, monkeypatch, owner_pid):
|
||||
def test_a_standalone_owner_is_the_per_profile_topology_not_a_refusal(tmp_path, monkeypatch, owner_pid, caplog):
|
||||
"""The owner answers ``multiplex: False``: it is a per-profile gateway, not a multiplexer that
|
||||
excluded us. Refusing here (exit 78 → launchd parks the unit) took every other profile's
|
||||
supervised gateway down at boot on a one-process-per-profile fleet. Start as before."""
|
||||
@@ -159,7 +159,9 @@ def test_a_standalone_owner_is_the_per_profile_topology_not_a_refusal(tmp_path,
|
||||
monkeypatch.setattr("gateway.control_socket.rescan_gateway_profiles",
|
||||
lambda home, timeout=8.0: {"multiplex": False, "served_profiles": ["tank"]})
|
||||
|
||||
assert host_attach.decide(tmp_path / "root" / "profiles" / "nous").outcome == host_attach.START
|
||||
with caplog.at_level("WARNING", logger="gateway.host_attach"):
|
||||
assert host_attach.decide(tmp_path / "root" / "profiles" / "nous").outcome == host_attach.START
|
||||
assert any("migrate --multiplex" in r.getMessage() for r in caplog.records), "the converge hint is logged"
|
||||
assert asyncio.run(gateway_run._host_attach_or_none(replace=False)) is None
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,23 @@ def test_a_supervised_attach_is_retried_not_parked(monkeypatch, capsys):
|
||||
assert exc.value.code != gw.GATEWAY_FATAL_CONFIG_EXIT_CODE
|
||||
|
||||
|
||||
def test_a_refusal_lands_in_the_profile_logs_not_only_on_stdout(monkeypatch, caplog):
|
||||
"""The refusal's stdout goes to the supervisor's unit log; launchd then maps 78 to a clean exit and
|
||||
parks the unit. Nothing in the profile's own logs said why (field report on #118097)."""
|
||||
owner = host_attach.HostGateway(4321, Path("/somewhere"), ("default",))
|
||||
monkeypatch.setattr(
|
||||
"gateway.host_attach.decide",
|
||||
lambda home, replace=False: host_attach.HostAttachDecision(
|
||||
host_attach.REFUSE, host_attach._refuse_message(owner, "nous"), owner))
|
||||
|
||||
with caplog.at_level("WARNING", logger="hermes_cli.gateway"), pytest.raises(SystemExit) as exc:
|
||||
gw._attach_to_host_gateway_or_guard(force=False)
|
||||
|
||||
assert exc.value.code == gw.GATEWAY_FATAL_CONFIG_EXIT_CODE
|
||||
warned = [r for r in caplog.records if r.levelno >= 30 and "migrate --multiplex" in r.getMessage()]
|
||||
assert warned, "a refusal must leave the remedy in the profile's own log"
|
||||
|
||||
|
||||
def test_replace_is_not_eaten_by_the_cli_guard(monkeypatch):
|
||||
"""The guard exited before ``start_gateway`` ever saw ``--replace``, so nothing was replaced."""
|
||||
owner = host_attach.HostGateway(4321, Path("/somewhere"), ("default", "other"))
|
||||
|
||||
Reference in New Issue
Block a user