fix(lifecycle-ledger): pre-stamp sentinels still tell an owner from a PID reuser
Review follow-up: a sentinel written before the create_time stamp has only the claim time, but that is epoch seconds too, and the owner was born before it claimed while a reuser was born after the owner died. One-sided compare instead of trusting any live PID. Dead monkeypatch line removed from the test.
This commit is contained in:
+15
-10
@@ -104,16 +104,17 @@ def _append_exit_diag(record: Dict[str, Any], home: Optional[Path]) -> None:
|
||||
logger.debug("Failed to append unclean-exit record", exc_info=True)
|
||||
|
||||
|
||||
def _pid_is_sentinel_owner(pid: Any, create_time: Any) -> bool:
|
||||
def _pid_is_sentinel_owner(pid: Any, start_time: Any, create_time: Any) -> bool:
|
||||
"""True when ``pid`` is a live process that is the sentinel's incarnation — guards the
|
||||
``--replace`` race: a live matching owner mid-teardown is a handover, not a death.
|
||||
|
||||
Identity is the psutil ``create_time`` the sentinel stamps at claim (epoch seconds, same
|
||||
producer on both sides). The sentinel's ``start_time`` is the ledger claim time and is NOT comparable with
|
||||
``gateway.status.get_process_start_time`` (proc ticks on Linux, centiseconds elsewhere) — the
|
||||
old comparison never matched, so every ``--replace`` handover read as an unclean death. A
|
||||
sentinel without ``create_time`` (pre-stamp gateway) cannot disambiguate PID reuse; a live PID
|
||||
is taken as the owner, matching the psutil-silent case below."""
|
||||
producer on both sides). The sentinel's ``start_time`` is the ledger claim time and is NOT
|
||||
comparable with ``gateway.status.get_process_start_time`` (proc ticks on Linux, centiseconds
|
||||
elsewhere) — the old comparison never matched, so every ``--replace`` handover read as an
|
||||
unclean death. A pre-stamp sentinel (no ``create_time``) still has ``start_time`` in epoch
|
||||
seconds: the owner was born BEFORE it claimed, a PID reuser AFTER the owner died, so a birth
|
||||
later than the claim is a reuser."""
|
||||
try:
|
||||
pid_int = int(pid)
|
||||
# NOT os.kill(pid, 0): on Windows that sends CTRL_C_EVENT to the target's console group.
|
||||
@@ -123,12 +124,16 @@ def _pid_is_sentinel_owner(pid: Any, create_time: Any) -> bool:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
if type(create_time) not in (int, float):
|
||||
return True
|
||||
from hermes_cli.process_identity import _process_create_time
|
||||
|
||||
actual = _process_create_time(pid_int)
|
||||
return actual is None or abs(actual - float(create_time)) <= 2.0 # None: can't disambiguate PID reuse
|
||||
if actual is None:
|
||||
return True # can't disambiguate PID reuse
|
||||
if type(create_time) in (int, float):
|
||||
return abs(actual - float(create_time)) <= 2.0
|
||||
if type(start_time) in (int, float):
|
||||
return actual <= float(start_time) + 2.0
|
||||
return True
|
||||
|
||||
|
||||
def _suspected_oom(mem: Dict[str, Any]) -> bool:
|
||||
@@ -149,7 +154,7 @@ def detect_unclean_exit(home: Optional[Path] = None) -> Optional[Dict[str, Any]]
|
||||
sentinel = _read_json(get_lifecycle_sentinel_path(home))
|
||||
if not sentinel or sentinel.get("phase") != "running":
|
||||
return None
|
||||
if _pid_is_sentinel_owner(sentinel.get("pid"), sentinel.get("create_time")):
|
||||
if _pid_is_sentinel_owner(sentinel.get("pid"), sentinel.get("start_time"), sentinel.get("create_time")):
|
||||
return None # live owner — planned takeover in flight, not a death
|
||||
evidence: Dict[str, Any] = {
|
||||
"prior_pid": sentinel.get("pid"), "prior_started_at": sentinel.get("started_at"),
|
||||
|
||||
Reference in New Issue
Block a user