fix(reset): never take another live reset's fence, and fence absent row registries (#78)
Review round 4 (codex): - R1: fence_registries reads an existing marker under the registry lock and refuses the reset, changing nothing, when another live (or undecidable) reset holds it - registries may be shared between workspaces; only a stale marker is replaced, and the reset's exit removes only its own fences. - R2: the registries to fence come from update.jobs_dirs, the enumeration open_jobs now uses too, whether or not they exist; an absent row registry is created and fenced (a registration would create it), and one that cannot be created refuses the reset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
568385c77a
commit
b49314c3d0
6 files changed
+213
-74
No files matched your search
@@ -94,32 +94,45 @@ def _lstart(pid: str) -> str:
|
||||
return " ".join(out.split())
|
||||
|
||||
|
||||
def reset_refusal(registry_dir: str, agent_session: str) -> Optional[str]:
|
||||
"""Why registering a job for ``agent_session`` in ``registry_dir`` is
|
||||
refused, or None: a live reset fenced this registry (for that session) and
|
||||
the caller is not its own create. Holder states as loop_lock.sh decides
|
||||
them: a dead or reused pid is stale; ps failing on this process is unknown
|
||||
(refused)."""
|
||||
def reset_marker(registry_dir: str) -> Tuple[str, Dict[str, str]]:
|
||||
"""(holder state, fields) of the registry's RESET_MARKER: "absent", or as
|
||||
loop_lock.sh decides - "stale" (dead or reused pid), "live", "unknown" (ps
|
||||
fails on this process; a marker that cannot be read is unknown too)."""
|
||||
marker = os.path.join(registry_dir, RESET_MARKER)
|
||||
try:
|
||||
with open(marker, encoding="utf-8") as fh:
|
||||
fields = dict(line.split("=", 1) for line in fh.read().splitlines() if "=" in line)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
return "absent", {}
|
||||
except (OSError, ValueError) as exc:
|
||||
return "the reset marker %s cannot be read (%s)" % (marker, exc)
|
||||
if "sessions" in fields and agent_session not in fields["sessions"].split(","):
|
||||
return None
|
||||
return "unknown", {"error": str(exc)}
|
||||
pid = fields.get("pid", "")
|
||||
if not pid.isdigit() or os.environ.get(RESET_OWNER_ENV) == pid:
|
||||
return None
|
||||
if not pid.isdigit():
|
||||
return "stale", fields
|
||||
if not _lstart(str(os.getpid())):
|
||||
return "cannot tell whether the reset in %s (pid %s) is live (ps unavailable?)" % (marker, pid)
|
||||
return "unknown", fields
|
||||
cur = _lstart(pid)
|
||||
if not cur or (fields.get("lstart") and cur != fields["lstart"]):
|
||||
return "stale", fields
|
||||
return "live", fields
|
||||
|
||||
|
||||
def reset_refusal(registry_dir: str, agent_session: str) -> Optional[str]:
|
||||
"""Why registering a job for ``agent_session`` in ``registry_dir`` is
|
||||
refused, or None: a live (or undecidable) reset fenced this registry, for
|
||||
that session, and the caller is not its own create."""
|
||||
state, fields = reset_marker(registry_dir)
|
||||
if state in ("absent", "stale"):
|
||||
return None
|
||||
if "sessions" in fields and agent_session not in fields["sessions"].split(","):
|
||||
return None
|
||||
if fields.get("pid") and os.environ.get(RESET_OWNER_ENV) == fields["pid"]:
|
||||
return None
|
||||
if state == "unknown":
|
||||
return "cannot tell whether the reset fencing %s is live (%s)" % (
|
||||
os.path.join(registry_dir, RESET_MARKER), fields.get("error") or "ps unavailable?")
|
||||
return ("a reset of %s is in progress (pid %s); its agents are being purged and recreated;"
|
||||
" register the job when it has finished" % (fields.get("workspace", "?"), pid))
|
||||
" register the job when it has finished" % (fields.get("workspace", "?"), fields["pid"]))
|
||||
|
||||
|
||||
def register_job(
|
||||
|
||||
Reference in new issue
Block a user