fix(issue-50): a waiter whose own attempt is already running answers clear, not superseded

The registry wait judged its record by one rule: not this attempt's
pending record in this session -> superseded (redispatched, moved or
closed by someone else). That also caught the record's own `started`
landing before the prompt went out - the Issue #14 registry ack winning
over an unreachable broker, as test_issue30_stall_watch_stop acknowledges
at registration - and the wrapper left on exit 5 instead of its
broker_unavailable path (rc 3).

A record that is running under this attempt's token in this session is
this attempt's own progress: the waiter answers `clear` at once, writes
no admission and starts no hold. Another token, another session or a
terminal status stay `superseded` (review 55219be1 R1 unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
GodopuandClaude Fable 5.1 committed 2026-10-11 06:28:01 +00:00
1 parent 7844ae2c5f
commit d50610c1ab
2 files changed
+42 -3

No files matched your search

+16 -3
View File
@@ -334,6 +334,16 @@ def _own(rec: Optional[dict], token: Optional[str], session: str) -> bool:
and _bare(str(rec.get("agent_session") or "")) == _bare(session) and _bare(str(rec.get("agent_session") or "")) == _bare(session)
def _started_own(rec: Optional[dict], token: Optional[str], session: str) -> bool:
"""This attempt's record, in this session, already `running`: its own
`started` landed before the prompt went out (the registry ack of Issue
#14 wins over an unreachable broker; a test that acknowledges at
registration). Nothing was redispatched, moved or closed by someone else
- there is just nothing left to wait for."""
return bool(rec) and bool(token) and rec.get("status") == "running" and rec.get("dispatch_token") == token \
and _bare(str(rec.get("agent_session") or "")) == _bare(session)
def _merge_own(registry_dir: str, job_id: str, token: Optional[str], session: str, source: str, fields: dict, def _merge_own(registry_dir: str, job_id: str, token: Optional[str], session: str, source: str, fields: dict,
guard=None) -> bool: guard=None) -> bool:
"""One merge through the engine, committed only while the record re-read """One merge through the engine, committed only while the record re-read
@@ -434,7 +444,9 @@ def wait_open_jobs(a: argparse.Namespace) -> str:
and its admission is committed (_admit); `busy <ids>` when the bound runs and its admission is committed (_admit); `busy <ids>` when the bound runs
out first; `superseded` once the record is not this attempt's pending one out first; `superseded` once the record is not this attempt's pending one
in this session any more (redispatched, moved, closed by someone else) - in this session any more (redispatched, moved, closed by someone else) -
nothing is written or closed then, the record is its owner's. The bound nothing is written or closed then, the record is its owner's; `clear`
at once, with no admission, when it is this attempt's and already
running (_started_own). The bound
is min(--wait-s, this job's remaining wall clock). The identity is pinned is min(--wait-s, this job's remaining wall clock). The identity is pinned
at the start: --dispatch-token (the token the wrapper read for this at the start: --dispatch-token (the token the wrapper read for this
attempt), else the record's token read once. Every DISPATCH_SIGNAL_EVERY s attempt), else the record's token read once. Every DISPATCH_SIGNAL_EVERY s
@@ -450,8 +462,9 @@ def wait_open_jobs(a: argparse.Namespace) -> str:
last_signal = None last_signal = None
while True: while True:
now = _now() now = _now()
if not _own(_load_record(a.registry_dir, a.job), token, a.agent_session): rec = _load_record(a.registry_dir, a.job)
return "superseded" if not _own(rec, token, a.agent_session):
return "clear" if _started_own(rec, token, a.agent_session) else "superseded"
live, stale = open_jobs(a.registry_dir, a.agent_session, a.job, now) live, stale = open_jobs(a.registry_dir, a.agent_session, a.job, now)
for other, reason in stale: for other, reason in stale:
if other not in reported: if other not in reported:
+26
View File
@@ -422,6 +422,32 @@ def test_u3g_a_waiter_whose_attempt_changed_in_place_leaves_the_admission_to_the
proc.communicate() proc.communicate()
def test_u3i_an_attempt_already_running_under_its_own_token_is_clear_not_superseded(tmp_path):
# the Issue #14 registry ack: the agent's `started` for this attempt lands
# before the prompt goes out (test_issue30_stall_watch_stop). Nothing to
# wait for, nothing foreign: `clear` at once, no admission written
a = _register(tmp_path, status="running")
_event(tmp_path, a, time.time())
me = _register(tmp_path)
tok = _rec(tmp_path, me)["dispatch_token"]
_edit(tmp_path, me, status="running")
before = _rec(tmp_path, me)
t0 = time.time()
assert decide.wait_open_jobs(_ns(tmp_path, me, wait_s=30)) == "clear"
assert time.time() - t0 < 5 and _rec(tmp_path, me) == before, "no wait, no write"
res = _cli(tmp_path, "wait-open-jobs", "--job", me, "--agent-session", "herdr:s", "--registry-dir", str(tmp_path),
"--wait-s", "30", "--timeout", "3600", "--dispatch-token", tok, "--hold")
assert res.stdout.strip() == "clear" and not _hold_pids(me), res.stderr
# running under another attempt's token, or in another session, is still someone else's
assert decide.wait_open_jobs(_ns(tmp_path, me, session="herdr:t")) == "superseded"
_edit(tmp_path, me, dispatch_token="another-attempt")
res = _cli(tmp_path, "wait-open-jobs", "--job", me, "--agent-session", "herdr:s", "--registry-dir", str(tmp_path),
"--wait-s", "30", "--timeout", "3600", "--dispatch-token", tok)
assert res.stdout.strip() == "superseded", res.stderr
_edit(tmp_path, me, dispatch_token=tok, status="completed")
assert decide.wait_open_jobs(_ns(tmp_path, me)) == "superseded"
def test_u3h_the_pinned_token_is_the_wrappers_not_the_records(tmp_path): def test_u3h_the_pinned_token_is_the_wrappers_not_the_records(tmp_path):
# the record's token changed before the waiter started (a redispatch raced # the record's token changed before the waiter started (a redispatch raced
# the wrapper): the waiter given the old token is superseded at once # the wrapper): the waiter given the old token is superseded at once