fix(gateway): send cron store notices for a symlinked cron/ directory

Follow-up to #133283 (greptile review). Store records are keyed by the
resolved store path, but the notice sender rebuilt the owning profile from
that path's parent. A profile whose cron/ is a symlink to another disk has
a foreign parent, so neither its outage nor recovery notice reached any
home channel; forget_homes had the same mismatch and never dropped such a
departed profile's record.

Both now match by resolving each served home's own cron/ path.
This commit is contained in:
kshitijk4poor
2026-10-07 16:29:18 +05:30
committed by kshitij
parent 70be36ada2
commit 0a8aee587d
3 changed files with 9 additions and 6 deletions

View File

@@ -165,11 +165,10 @@ def outage_covers(cron_dir: Path, due_at: float, grace: float) -> bool:
def forget_homes(home_keys) -> None:
"""Drop the state of stores whose profile home (``hermes_home_key``) this process no longer
ticks, so a profile that left this gateway cannot keep the host-wide gauges at writable=0."""
from hermes_constants import hermes_home_key
keys = set(home_keys)
stores = {_key(Path(home) / "cron") for home in home_keys} # a symlinked cron/ resolves elsewhere
with _lock:
for records in (_degraded, _recovered):
for store in [s for s in records if hermes_home_key(Path(s).parent) in keys]:
for store in [s for s in records if s in stores]:
del records[store]

View File

@@ -95,12 +95,13 @@ async def send_cron_store_notice(runner, event: str, record) -> None:
fields = record.notice_fields()
key = "gateway.cron_store.unwritable" if event == "unwritable" else "gateway.cron_store.recovered"
store_home = hermes_home_key(Path(record.store).parent)
store_key = hermes_home_key(record.store)
served_homes = runner._served_profile_homes or {}
logger.info("Broadcasting cron store %s notice for %s", event, record.store)
for profile, platform, _cfg, home, transport in list(runner._served_home_channel_transports()):
served_home = served_homes.get(profile) if profile is not None else None
if hermes_home_key(served_home or get_routing_process_hermes_home()) != store_home:
# record.store is resolved: a cron/ symlinked to another disk has a foreign parent.
if hermes_home_key(Path(served_home or get_routing_process_hermes_home()) / "cron") != store_key:
continue
# The opt-out is the owning profile's; the launch profile needs no extra scope.
scope = _async_profile_runtime_scope(Path(served_home)) if served_home else contextlib.nullcontext()

View File

@@ -380,7 +380,10 @@ def test_unwritable_store_is_shown_in_cron_status_and_announced_once(cron_store,
return True
home = SimpleNamespace(chat_id="c1", thread_id=None)
runner = SimpleNamespace(_send_home_channel_message=send, _served_profile_homes={"p": cron_store},
profile = cron_store / "profile" # its cron/ is a symlink to the store on another disk
profile.mkdir()
(profile / "cron").symlink_to(cron_dir, target_is_directory=True)
runner = SimpleNamespace(_send_home_channel_message=send, _served_profile_homes={"p": profile},
_served_home_channel_transports=lambda: iter([("p", "telegram", None, home, object())]))
monkeypatch.setattr("hermes_constants.get_routing_process_hermes_home", lambda: cron_store / "launch")
# A manual loop clock: the real one-hour "recovered" window only elapses when the test says