docs(gateway): drop the remaining whichever-home --replace claims

The host_attach module doc, the REPLACE_HOST consumer comment in
_host_attach_or_none and the _refuse_second_host_gateway refusal still
promised the old semantics: --replace takes the host over from whichever
home launched it. Since decide() only returns REPLACE_HOST for an owner
that serves this profile (or has not published its served set), and the
lock claim no longer treats --replace as --force, that advice is untrue
on exactly the path where it was printed.

_refuse_second_host_gateway is reached after losing the host lock to a
live process -- including one with no readable record (publish_record
failed after the claim, or a different HOST_PROTOCOL_VERSION during a
rolling upgrade). Offering --replace there sends the operator around a
loop that ends in the same exit 75. The message now says to stop the
other gateway or use --force, and states that --replace does not skip
the lock check. Text/comment changes only; no behaviour change.
This commit is contained in:
kshitijk4poor
2026-09-23 17:33:31 +05:30
committed by kshitij
parent 4cec3f85e7
commit 0717586e55
2 changed files with 21 additions and 6 deletions

View File

@@ -10,7 +10,10 @@ Five outcomes, in order:
* ``ATTACH`` — a live host gateway already serves this profile. Nothing to start; exit 0.
* ``RESCAN``→ATTACH — it does not serve it yet: ask it to reconcile ``profiles/`` now (control
socket ``rescan-profiles``) and attach once the answer includes us.
* ``REPLACE_HOST`` — ``--replace`` names the host process as the target, whichever home launched it.
* ``REPLACE_HOST`` — ``--replace`` names the host process as the target when it serves this
profile (or has not published its served set yet), whichever home launched it. An owner known
not to serve us is another profile's gateway and falls through to REFUSE/START below;
``--force`` is the explicit takeover-anything switch.
* ``REFUSE`` — a live MULTIPLEXING gateway exists and cannot be made to serve this profile.
Never start a second one silently.
* ``START`` — no live owner, or the owner answers ``multiplex: False``: it is another

View File

@@ -5482,7 +5482,14 @@ def _owner_is_standalone() -> bool:
def _refuse_second_host_gateway(owner) -> None:
"""Print the named refusal and exit 75 so a supervisor retries instead of parking the unit."""
"""Print the named refusal and exit 75 so a supervisor retries instead of parking the unit.
Reached only after losing the host lock to a live process, so ``--replace`` is not offered as
the way past it: it only takes over an owner that serves this profile (which would have been
handled before the claim), and it does not skip the lock check. A holder with no readable
record (its publish failed, or it runs another HOST_PROTOCOL_VERSION) can only be stopped or
bypassed with ``--force``.
"""
from gateway import host_rendezvous as hr
from gateway.restart import GATEWAY_SERVICE_RESTART_EXIT_CODE
from hermes_cli.gateway_migrate import MIGRATE_COMMAND
@@ -5493,8 +5500,9 @@ def _refuse_second_host_gateway(owner) -> None:
f" Exactly one gateway per host serves every profile, so this process will not start a\n"
f" second one (it would double-bind this profile's platforms).\n"
f" Fold every profile onto the owner: {MIGRATE_COMMAND}\n"
f" Or take the host over: hermes gateway run --replace\n"
f" Or start one anyway: hermes gateway run --force")
f" Or stop the other gateway first, then start this one.\n"
f" Or start one anyway (skips the host-lock check): hermes gateway run --force\n"
f" (--replace does not skip this check; it only replaces an owner that serves this profile.)")
logger.error("Refusing to start a second gateway on this host: %s", who)
print(message)
raise SystemExit(GATEWAY_SERVICE_RESTART_EXIT_CODE)
@@ -5573,7 +5581,8 @@ async def _host_attach_or_none(replace: bool, force: bool = False) -> Optional[b
print(decision.message)
return False
if decision.outcome == REPLACE_HOST and decision.owner is not None:
# --replace names the HOST process, whichever home launched it.
# --replace names the HOST process, whichever home launched it, but only when it serves
# this profile or has not published its served set yet (decide() already filtered).
if not await _start_gateway_replace_existing_instance(decision.owner.pid, True):
return False
return None
@@ -5876,7 +5885,10 @@ async def start_gateway(config: Optional[GatewayConfig] = None, replace: bool =
# PID file BEFORE adapters: of two concurrent `run --replace`, only the O_EXCL winner opens sockets.
# Only --force skips the host-lock refusal. Every generated unit carries --replace, so reading it as
# --force there disabled the one arbiter of the two-units-at-once race; a replace that took the
# owner over already freed the lock with that process.
# owner over already freed the lock with that process. Consequence: a live holder with no
# readable record (publish_record failed after the claim, or a different HOST_PROTOCOL_VERSION
# during a rolling upgrade) blocks every other unit with exit 75 until it exits; only --force
# gets past it.
if not _start_gateway_claim_pid_file(force=force):
return False