fix(issue-50): review findings of PR #56 on macOS
- admission: a `submit --dry-run` record is marked dry_run and is not an open job of its session (review 1e25cf2f F1) - create: the monitor-row adoption requires the row's server to be the one this create spawned on (review 1e25cf2f F2) - pane lock: retry only on contention (BlockingIOError); the docs say the flock lasts while any fd-9 holder (the claim shell and the shim) lives (review 66680d9d) - tests on macOS: real-herdr box HOMEs under /tmp (sun_path is 104 bytes), resolved paths, the fake claude reads non-canonical input (MAX_CANON 1024) and the pane is read unwrapped; lock holders exec their sleep so killing them frees the lock; the adoption tests name the server Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCgu9Fa9iT7TbJUSzdq4Ff
This commit is contained in:
1 parent
320aa9f7a0
commit
a9ffedac36
10 files changed
+75
-19
No files matched your search
@@ -1495,8 +1495,10 @@ _herdr_scope_is_pane_inherited() {
|
||||
# later `agent start` failed with agent_pane_busy. mam_herdr runs the whole
|
||||
# new-session call holding one lock per server: a flock on fd 9, opened for
|
||||
# the block below and taken by lib_py.pane_lock (which also bounds the wait,
|
||||
# MAM_PANE_CLAIM_WAIT_S). It ends with the block or with this process, however
|
||||
# that ends, so a dead holder leaves nothing behind; the file is the target
|
||||
# MAM_PANE_CLAIM_WAIT_S). It ends with the block, or when every process that
|
||||
# holds fd 9 - this shell and the shim it runs - has ended, however that ends,
|
||||
# so a dead holder leaves nothing behind. Nothing long-lived may inherit fd 9
|
||||
# (the herdr server is spawned with close_fds; review 66680d9d); the file is the target
|
||||
# server's, so other servers' creates are not held. The lock lives here, not
|
||||
# in the shim heredoc, which stays byte-identical (test_issue23_24 S11).
|
||||
#
|
||||
|
||||
@@ -90,6 +90,7 @@ def register_job(
|
||||
transport: str = "herdr",
|
||||
supersede_older_attempt: bool = False,
|
||||
parent_job: Optional[str] = None,
|
||||
dry_run: bool = False,
|
||||
) -> str:
|
||||
"""Create a new ``pending`` job record and return its id.
|
||||
|
||||
@@ -106,6 +107,10 @@ def register_job(
|
||||
|
||||
Issue #34: ``parent_job`` names the job this one was delegated from, so a
|
||||
loop waiting on the parent can find and wait for it. None is a root job.
|
||||
|
||||
Issue #50 (review 1e25cf2f F1): ``dry_run`` marks the record of a
|
||||
``submit --dry-run``, which is never dispatched, so the registry admission
|
||||
does not count it as an open job of its session.
|
||||
"""
|
||||
job_id = job_id or generate_job_id(bits)
|
||||
heartbeat_sec = max(1, int(heartbeat_sec))
|
||||
@@ -152,6 +157,8 @@ def register_job(
|
||||
# Issue #34: the job this one was delegated from (None = a root job)
|
||||
"parent_job": parent_job,
|
||||
}
|
||||
if dry_run:
|
||||
record["dry_run"] = True
|
||||
with registry_lock(registry_dir):
|
||||
if mqtt_common._job_path(job_id, registry_dir).exists():
|
||||
if not supersede_older_attempt:
|
||||
@@ -393,6 +400,8 @@ def _build_parser() -> argparse.ArgumentParser:
|
||||
p_reg.add_argument("--reviewer", default=None)
|
||||
p_reg.add_argument("--reviewer-session", default=None)
|
||||
p_reg.add_argument("--max-iterations", type=int, default=5)
|
||||
p_reg.add_argument("--dry-run", action="store_true",
|
||||
help="the record of a submit --dry-run: never dispatched, not an open job for admission")
|
||||
p_reg.add_argument("--parent-job", default=None,
|
||||
help="the job this one is delegated from (Issue #34); must exist in this registry")
|
||||
|
||||
@@ -569,6 +578,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
attempt_id=args.attempt_id,
|
||||
job_id=args.job_id,
|
||||
parent_job=args.parent_job,
|
||||
dry_run=args.dry_run,
|
||||
)
|
||||
print(job_id)
|
||||
return 0
|
||||
|
||||
@@ -297,6 +297,8 @@ def open_jobs(registry_dir: str, session: str, job_id: str, now: Optional[int] =
|
||||
rec = _load_record(registry_dir, other)
|
||||
if rec is None or _bare(str(rec.get("agent_session") or "")) != sess:
|
||||
continue
|
||||
if rec.get("dry_run"): # a submit --dry-run is never dispatched (review 1e25cf2f F1)
|
||||
continue
|
||||
status = rec.get("status")
|
||||
if status == "running":
|
||||
idle = _int_field(rec, "idle_timeout_sec", DEFAULT_IDLE_TIMEOUT_SEC)
|
||||
|
||||
@@ -19,9 +19,12 @@ one lock per server from the choice to the claim.
|
||||
`locked`, or `timeout` once MAM_PANE_CLAIM_WAIT_S
|
||||
(default 120, minimum 1) has passed
|
||||
|
||||
The lock is a BSD flock, which belongs to the open file the shim holds on fd 9:
|
||||
it outlives this helper and ends with the shim's process, however that ends
|
||||
(exit, kill -9), so a dead holder leaves nothing to steal. flock(1) is not on
|
||||
The lock is a BSD flock, which belongs to the open file on fd 9: it outlives
|
||||
this helper and ends when the last process holding that fd ends, however it
|
||||
ends (exit, kill -9), so a dead holder leaves nothing to steal. The holders are
|
||||
the shell running the claim block and the command it runs (the shim); a child
|
||||
that outlived them would keep it too, so nothing long-lived may inherit fd 9 -
|
||||
the herdr server is spawned with close_fds (review 66680d9d). flock(1) is not on
|
||||
macOS; fcntl.flock is. Stdlib only, Python 3.9.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
@@ -67,13 +70,14 @@ def wait_s(env: Mapping[str, str]) -> int:
|
||||
def acquire(fd: int, wait: float) -> str:
|
||||
"""`locked`, or `timeout` when the lock was not free within `wait` s. The
|
||||
deadline is on the monotonic clock; a step of the wall clock does not
|
||||
stretch or cut it."""
|
||||
stretch or cut it. Only contention is waited out: any other error (a bad
|
||||
fd) raises, which the shim reports as an undecided lock."""
|
||||
deadline = time.monotonic() + wait
|
||||
while True:
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
return "locked"
|
||||
except OSError:
|
||||
except BlockingIOError:
|
||||
if time.monotonic() >= deadline:
|
||||
return "timeout"
|
||||
time.sleep(0.2)
|
||||
|
||||
@@ -533,9 +533,13 @@ running_same = [s for s in sessions if s.get('name') == name and s.get('status')
|
||||
# registered, with no job, for a pane created after this create began, is
|
||||
# this create's own pane - herdr keeps live names unique per server and the
|
||||
# preflight refused an older session of this name - so it is replaced, not
|
||||
# refused as a second live session.
|
||||
# refused as a second live session. Names are unique per server only, so the
|
||||
# row must be this server's (review 1e25cf2f F2: another server's row of the
|
||||
# name was replaced). The pane pid is no identity: it is the pane's foreground
|
||||
# process at the time it was read.
|
||||
def _adopted_own(s):
|
||||
return (s.get('last_visible_note') == 'auto-registered by monitor' and not s.get('delegate_job_id')
|
||||
and (s.get('herdr_server') or s.get('herdr_session') or 'default') == server_name
|
||||
and epoch.isdigit() and int(s.get('herdr_session_epoch') or 0) >= int(epoch) - 1)
|
||||
if running_same and all(_adopted_own(s) for s in running_same):
|
||||
print(f"NOTE: the monitor registered {name} while it was being created; that row is replaced by this one", flush=True)
|
||||
|
||||
@@ -292,6 +292,10 @@ cmd_submit() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Issue #50: a dry-run's record is never dispatched, so admission skips it
|
||||
local reg_dry=()
|
||||
if [[ "$DRY_RUN" == "1" ]]; then reg_dry=(--dry-run); fi
|
||||
|
||||
# 1) register job (prints the new job id)
|
||||
JOB_ID="$("$PY" "$SCRIPT_DIR/scripts/registry.py" --registry-dir "$REGISTRY_DIR" register \
|
||||
"${reg_prompt[@]}" --agent "$AGENT" --agent-session "$AGENT_SESSION" --role "$DELEGATE_ROLE" \
|
||||
@@ -299,7 +303,7 @@ cmd_submit() {
|
||||
--heartbeat-sec "$HEARTBEAT_SEC" --heartbeat-misses "$HEARTBEAT_MISSES" --ack-timeout "$ACK_TIMEOUT" \
|
||||
--transport "$(_is_remote "$AGENT_SESSION" && echo remote || echo herdr)" \
|
||||
--job-type "$TYPE" --reviewer "$REVIEWER" --reviewer-session "$REVIEWER_SESSION" \
|
||||
--max-iterations "$MAX_ITERATIONS" ${reg_parent[@]+"${reg_parent[@]}"})"
|
||||
--max-iterations "$MAX_ITERATIONS" ${reg_parent[@]+"${reg_parent[@]}"} ${reg_dry[@]+"${reg_dry[@]}"})"
|
||||
echo "registered job: $JOB_ID"
|
||||
|
||||
# 1-1) Provision job directory and write direct brief.md (MAM Job Restructuring)
|
||||
|
||||
Reference in new issue
Block a user