fix(cron): treat a live multiplexer as gateway-alive for satellite profiles

A named profile has no local gateway.pid, so cron warned that jobs
would not fire and recommended hermes gateway install — which the
start guard then refuses with exit 78. Share the multiplexer-serving
probe with the start guard and count it as liveness.
This commit is contained in:
686f6c61
2026-08-29 02:58:57 +02:00
committed by Teknium
parent 5742f6987d
commit e703717513
4 changed files with 143 additions and 77 deletions
+9 -2
View File
@@ -93,9 +93,16 @@ def _builtin_gateway_liveness() -> Optional[bool]:
# scan below still decide instead of collapsing the whole
# tri-state to None.
pass
from hermes_cli.gateway import find_gateway_pids
from hermes_cli.gateway import (
find_gateway_pids,
named_profile_served_by_running_multiplexer,
)
return bool(find_gateway_pids())
if find_gateway_pids():
return True
# Satellite profile: no local gateway.pid, but the default multiplexer
# ticks this profile's cron store (#97120).
return named_profile_served_by_running_multiplexer()
except Exception:
return None
+77 -75
View File
@@ -6048,6 +6048,82 @@ def _running_under_gateway_supervisor() -> bool:
return is_gateway_supervisor_process()
def named_profile_served_by_running_multiplexer() -> bool:
"""True when a live default multiplexer already ticks this named profile.
Shared by the named-profile start guard and cron liveness: a satellite
profile has no gateway.pid of its own, but the default multiplexer's
ticker still fires its jobs (#97120).
"""
try:
suffix = _profile_suffix()
except Exception:
return False
if not suffix:
return False
try:
from hermes_constants import get_default_hermes_root
default_root = get_default_hermes_root()
except Exception:
return False
try:
from gateway.status import _read_pid_record
default_pid_path = default_root / "gateway.pid"
rec = _read_pid_record(default_pid_path)
if not rec:
return False
from gateway.status import _pid_exists, _pid_from_record
pid = _pid_from_record(rec)
if not pid or not _pid_exists(pid):
return False
from gateway.config import _env_multiplex_profiles_override
cfg_path = default_root / "config.yaml"
cfg = {}
if cfg_path.exists():
from hermes_cli.config import read_user_config_raw
cfg = read_user_config_raw(cfg_path)
env_multiplex = _env_multiplex_profiles_override()
if env_multiplex is False:
return False
if env_multiplex is True:
multiplex = True
else:
if not cfg_path.exists():
return False
multiplex = bool(
cfg.get("multiplex_profiles")
or (cfg.get("gateway", {}) or {}).get("multiplex_profiles")
)
if not multiplex:
return False
gateway_cfg = cfg.get("gateway", {}) or {}
if "multiplex_profile_allowlist" in cfg:
raw_allowlist = cfg.get("multiplex_profile_allowlist")
else:
raw_allowlist = gateway_cfg.get("multiplex_profile_allowlist")
from gateway.config import _normalize_multiplex_profile_allowlist
from hermes_cli.profiles import normalize_profile_name
profile_allowlist = _normalize_multiplex_profile_allowlist(raw_allowlist)
if (
profile_allowlist is not None
and normalize_profile_name(suffix) not in profile_allowlist
):
return False
return True
except Exception:
logger.debug("Multiplexer-serving probe failed", exc_info=True)
return False
def _guard_named_profile_under_multiplexer(force: bool = False) -> None:
"""Refuse a named-profile gateway when a multiplexer is already serving it.
@@ -6063,85 +6139,11 @@ def _guard_named_profile_under_multiplexer(force: bool = False) -> None:
"""
if force:
return
# (a) Are we a named profile? Default/custom-hash homes return "".
try:
suffix = _profile_suffix()
except Exception:
return
if not suffix:
return # default profile (or unrecognized) — this guard doesn't apply
try:
from hermes_constants import get_default_hermes_root
default_root = get_default_hermes_root()
# (b) Is the default-profile gateway running?
from gateway.status import get_running_pid as _default_running_pid # noqa
except Exception:
return
try:
import yaml as _yaml
from gateway.status import _read_pid_record # type: ignore
# (b) default gateway PID file present + alive
default_pid_path = default_root / "gateway.pid"
rec = _read_pid_record(default_pid_path)
if not rec:
return
from gateway.status import _pid_exists, _pid_from_record
pid = _pid_from_record(rec)
if not pid or not _pid_exists(pid):
return
# (c) multiplexing is on for the default gateway. Precedence mirrors
# gateway.config: the GATEWAY_MULTIPLEX_PROFILES env override wins over
# config.yaml when set to a recognized value, so a hosted gateway that
# forces multiplex on via env (with no multiplex_profiles in config.yaml)
# still trips this guard. A blank/unrecognized env value falls through
# to config.yaml.
from gateway.config import _env_multiplex_profiles_override
cfg_path = default_root / "config.yaml"
cfg = {}
if cfg_path.exists():
# Raw read of the DEFAULT root's config (not the active profile
# home, so load_config() is the wrong owner here); whole probe is
# fail-open via the enclosing except.
from hermes_cli.config import read_user_config_raw
cfg = read_user_config_raw(cfg_path)
env_multiplex = _env_multiplex_profiles_override()
if env_multiplex is False:
return # explicitly forced OFF by the operator env override
if env_multiplex is True:
multiplex = True
else:
if not cfg_path.exists():
return
multiplex = bool(
cfg.get("multiplex_profiles")
or (cfg.get("gateway", {}) or {}).get("multiplex_profiles")
)
if not multiplex:
return
gateway_cfg = cfg.get("gateway", {}) or {}
if "multiplex_profile_allowlist" in cfg:
raw_allowlist = cfg.get("multiplex_profile_allowlist")
else:
raw_allowlist = gateway_cfg.get("multiplex_profile_allowlist")
from gateway.config import _normalize_multiplex_profile_allowlist
from hermes_cli.profiles import normalize_profile_name
profile_allowlist = _normalize_multiplex_profile_allowlist(raw_allowlist)
if (
profile_allowlist is not None
and normalize_profile_name(suffix) not in profile_allowlist
):
return
except Exception:
logger.debug("Multiplexer-conflict probe failed", exc_info=True)
if not named_profile_served_by_running_multiplexer():
return
print_error(
@@ -199,6 +199,12 @@ class _LivenessPatches:
return_value=list(self._pids),
)
)
self._stack.enter_context(
patch(
"hermes_cli.gateway.named_profile_served_by_running_multiplexer",
return_value=False,
)
)
self._stack.enter_context(
patch(
"gateway.status.is_gateway_runtime_lock_active",
@@ -258,6 +264,10 @@ class TestRuntimeLockFirstLiveness:
patch("hermes_cli.cron._active_cron_provider_name", return_value="builtin"),
patch("gateway.status.is_gateway_runtime_lock_active", return_value=False),
patch("hermes_cli.gateway.find_gateway_pids", return_value=[]),
patch(
"hermes_cli.gateway.named_profile_served_by_running_multiplexer",
return_value=False,
),
):
assert cron_cli._builtin_gateway_liveness() is False
@@ -279,6 +289,39 @@ class TestRuntimeLockFirstLiveness:
):
assert cron_cli._builtin_gateway_liveness() is True
def test_running_multiplexer_counts_as_alive_for_named_profile(self):
"""A satellite profile has no own PID; the default multiplexer ticks it."""
from unittest.mock import patch
import hermes_cli.cron as cron_cli
with (
patch("hermes_cli.cron._active_cron_provider_name", return_value="builtin"),
patch("gateway.status.is_gateway_runtime_lock_active", return_value=False),
patch("hermes_cli.gateway.find_gateway_pids", return_value=[]),
patch(
"hermes_cli.gateway.named_profile_served_by_running_multiplexer",
return_value=True,
),
):
assert cron_cli._builtin_gateway_liveness() is True
def test_no_multiplexer_and_no_pids_is_still_false(self):
from unittest.mock import patch
import hermes_cli.cron as cron_cli
with (
patch("hermes_cli.cron._active_cron_provider_name", return_value="builtin"),
patch("gateway.status.is_gateway_runtime_lock_active", return_value=False),
patch("hermes_cli.gateway.find_gateway_pids", return_value=[]),
patch(
"hermes_cli.gateway.named_profile_served_by_running_multiplexer",
return_value=False,
),
):
assert cron_cli._builtin_gateway_liveness() is False
class TestCronStatusLockFirst:
"""`hermes cron status` shares the lock-first false-alarm fix (#95947).
+14
View File
@@ -122,4 +122,18 @@ class TestNamedProfileMultiplexerGuard:
gw._guard_named_profile_under_multiplexer(force=False)
def test_named_profile_served_probe_matches_the_start_guard(self, monkeypatch, tmp_path):
self._fake_running_default_gateway(monkeypatch, tmp_path)
(tmp_path / "config.yaml").write_text(
"gateway:\n multiplex_profiles: true\n",
encoding="utf-8",
)
from hermes_cli import gateway as gw
assert gw.named_profile_served_by_running_multiplexer() is True
monkeypatch.setattr(gw, "_profile_suffix", lambda: "")
assert gw.named_profile_served_by_running_multiplexer() is False