fix(reset): fence every job registry the reset reads, by its physical directory (#78)
Review round 3 R1 (codex): the registration guard chose the workspace marker from the path's spelling, so a symlink alias of .mam/jobs or a MAM_JOBS_DIR registry was not refused. The reset now writes .reset-in-progress into each registry its open-job check reads (.mam/jobs, MAM_JOBS_DIR, a row's other control_plane.mam_jobs_dir - the latter only for this workspace's sessions), each under that registry's lock and before the check, and removes it on exit; register_job reads the marker inside the registry it writes to. The precheck's separate lock barrier is subsumed by the fence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
548c6854ea
commit
568385c77a
6 files changed
+186
-75
No files matched your search
@@ -18,11 +18,12 @@ import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from typing import Dict, List, Mapping, Optional, Tuple
|
||||
from typing import Dict, Iterator, List, Mapping, Optional, Tuple
|
||||
|
||||
from lib_py.agents.base import AGENT_SESSION_ENV, POST_DISCOVERED
|
||||
from lib_py.agents.registry import agent_of_row, get_adapter, own_key
|
||||
from lib_py.delegation.mqtt_common import registry_lock
|
||||
from lib_py.delegation.registry import RESET_MARKER
|
||||
from lib_py.update import open_jobs, row_server
|
||||
from lib_py.verify_session import verify_session_uuid
|
||||
|
||||
@@ -484,6 +485,39 @@ def target_open_jobs(workspace: str, state: dict, env: Mapping[str, str], sessio
|
||||
return out
|
||||
|
||||
|
||||
def fence_registries(workspace: str, state: dict, env: Mapping[str, str]) -> Iterator[str]:
|
||||
"""Write RESET_MARKER into every registry open_jobs reads, each under its
|
||||
registry lock (a registration that read no marker has finished writing),
|
||||
and yield its real path as it is written. This workspace's .mam/jobs and
|
||||
MAM_JOBS_DIR (created when absent) refuse every registration; another
|
||||
control plane's registry named by a row refuses only those addressed to
|
||||
this registry's rows. The marker copies .mam/reset-in-progress."""
|
||||
with open(os.path.join(workspace, ".mam", "reset-in-progress"), encoding="utf-8") as fh:
|
||||
base = "".join(l + "\n" for l in fh.read().splitlines() if l.split("=", 1)[0] in ("pid", "lstart", "started"))
|
||||
base += "workspace=%s\n" % workspace
|
||||
rows = state.get("herdr_sessions") or []
|
||||
sessions = ",".join("herdr:%s" % s["name"] for s in rows if s.get("name"))
|
||||
plan, seen = [], set()
|
||||
for d in (os.path.join(workspace, ".mam", "jobs"), env.get("MAM_JOBS_DIR") or ""):
|
||||
if d:
|
||||
os.makedirs(d, exist_ok=True)
|
||||
plan.append((d, base))
|
||||
for s in rows:
|
||||
d = (s.get("control_plane") or {}).get("mam_jobs_dir") or ""
|
||||
if d and os.path.isdir(d):
|
||||
plan.append((d, base + "sessions=%s\n" % sessions))
|
||||
for d, text in plan:
|
||||
if _real(d) in seen:
|
||||
continue
|
||||
seen.add(_real(d))
|
||||
with registry_lock(d):
|
||||
fd, tmp = tempfile.mkstemp(dir=d, prefix=RESET_MARKER + ".")
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fh.write(text)
|
||||
os.replace(tmp, os.path.join(d, RESET_MARKER))
|
||||
yield _real(d)
|
||||
|
||||
|
||||
def target_id_owned(state: dict, session_name: str) -> bool:
|
||||
"""The row's recorded post-discovered id still verifies (revalidate) as a
|
||||
conversation of the row's cwd - the check stop makes before it keeps such
|
||||
@@ -757,6 +791,9 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
s.add_argument("--workspace", required=True)
|
||||
s.add_argument("--session", required=True)
|
||||
|
||||
s = sub.add_parser("fence")
|
||||
s.add_argument("--workspace", required=True)
|
||||
|
||||
s = sub.add_parser("id-owned")
|
||||
s.add_argument("--workspace", required=True)
|
||||
s.add_argument("--session", required=True)
|
||||
@@ -814,12 +851,6 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
return 1 if sel["errors"] else 0
|
||||
|
||||
if a.cmd == "precheck":
|
||||
# A registration that read no reset marker finishes writing under this
|
||||
# lock before the open jobs are listed (registry.reset_refusal).
|
||||
jobs_dir = os.path.join(ws, ".mam", "jobs")
|
||||
if os.path.isdir(jobs_dir):
|
||||
with registry_lock(jobs_dir):
|
||||
pass
|
||||
lines = precheck(ws, state, os.environ)
|
||||
for line in lines:
|
||||
print(line)
|
||||
@@ -852,6 +883,15 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
print(line)
|
||||
return 1 if lines else 0
|
||||
|
||||
if a.cmd == "fence":
|
||||
try:
|
||||
for d in fence_registries(ws, state, os.environ):
|
||||
print(d, flush=True)
|
||||
except OSError as exc:
|
||||
print("ERROR: cannot fence the job registries (%s)" % exc, file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
if a.cmd == "id-owned":
|
||||
return 0 if target_id_owned(state, a.session) else 1
|
||||
|
||||
|
||||
Reference in new issue
Block a user