fix(id_order): prune an orphaned marker only on an authoritative "gone", within the wait cap (Issue #63)
PR #69 review round 2. - The herdr probe is tri-state (present / absent / unknown) from a parsed `_herdr agent list`: absent only when the list parses without the name or herdr answers server_not_running. A failed, unparseable or timed-out lookup is unknown and keeps the marker (codex P1: has-session's exit 1 meant both "absent" and "lookup failed"). - Probes run with MAM_NO_AUTOSTART=1 so a check never starts a server (agy P2). - Probe time is capped by max(deadline, start + PROBE_FLOOR_S=2 s), shared by all markers, so a slow herdr cannot hold the wait or its lock past the cap (codex P2); MAM_ID_ORDER_WAIT_S=0 still gets one bounded look. - Tests: unknown probe keeps and refuses; delayed probe at caps 0/1/3 stays within the bound; real probe via lib.sh mam_id_order_wait against mock herdr (listed / unlisted / failing list / server_not_running). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ede9c62be3
commit
8f64174b6b
2 files changed
+179
-30
No files matched your search
@@ -31,9 +31,9 @@ pytestmark = pytest.mark.usefixtures("own_workspace_root", "no_herdr_probe")
|
||||
@pytest.fixture
|
||||
def no_herdr_probe(monkeypatch):
|
||||
"""In-process id_order.wait never asks the real herdr about a dead-owner
|
||||
marker's session: it reads as gone unless a test passes pane_alive."""
|
||||
marker's session: it reads as gone unless a test passes pane_state."""
|
||||
from lib_py.agents import id_order
|
||||
monkeypatch.setattr(id_order, "herdr_session_alive", lambda server, session: False)
|
||||
monkeypatch.setattr(id_order, "herdr_session_state", lambda server, session, timeout: False)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- helpers
|
||||
@@ -1065,15 +1065,18 @@ def test_id_order_dead_owner_marker_blocks_while_its_unregistered_herdr_session_
|
||||
marker_a = Path(dpath) / "orphan-a-agy.json"
|
||||
|
||||
alive_sessions = {"orphan-a-agy"}
|
||||
unknown_sessions = set()
|
||||
probes = []
|
||||
|
||||
def pane_alive(server, session):
|
||||
def pane_state(server, session, timeout):
|
||||
probes.append((server, session))
|
||||
if session in unknown_sessions:
|
||||
return None
|
||||
return session in alive_sessions
|
||||
|
||||
def wait_b(**kw):
|
||||
return id_order.wait(str(ws), "agy", "new-b-agy", os.getpid(), yaml_path=str(yaml_path),
|
||||
home=str(home), wait_s_val=0, pane_alive=pane_alive, **kw)
|
||||
home=str(home), wait_s_val=0, pane_state=pane_state, **kw)
|
||||
|
||||
dead = subprocess.Popen(["true"])
|
||||
dead.wait()
|
||||
@@ -1088,6 +1091,13 @@ def test_id_order_dead_owner_marker_blocks_while_its_unregistered_herdr_session_
|
||||
err = capsys.readouterr().err
|
||||
assert "HERDR_SESSION_NAME=srv-x herdr kill-session -t orphan-a-agy" in err
|
||||
|
||||
# No authoritative answer from herdr -> kept and blocks as well
|
||||
unknown_sessions.add("orphan-a-agy")
|
||||
assert wait_b() == 1
|
||||
assert marker_a.exists()
|
||||
assert "herdr could not tell whether its session is gone" in capsys.readouterr().err
|
||||
unknown_sessions.clear()
|
||||
|
||||
# Row registered for the attempt -> marker pruned without a probe; the row decides
|
||||
probes.clear()
|
||||
epoch = int(time.time())
|
||||
@@ -1110,12 +1120,12 @@ def test_id_order_dead_owner_marker_blocks_while_its_unregistered_herdr_session_
|
||||
same = Path(dpath) / "same-agy.json"
|
||||
id_order.write_marker(dpath, "same-agy", live.pid, str(ws), "agy")
|
||||
assert id_order.wait(str(ws), "agy", "same-agy", os.getpid(), yaml_path=str(yaml_path),
|
||||
home=str(home), wait_s_val=0, pane_alive=pane_alive) == 1
|
||||
home=str(home), wait_s_val=0, pane_state=pane_state) == 1
|
||||
assert json.loads(same.read_text())["owner_pid"] == live.pid
|
||||
live.kill()
|
||||
live.wait()
|
||||
assert id_order.wait(str(ws), "agy", "same-agy", os.getpid(), yaml_path=str(yaml_path),
|
||||
home=str(home), wait_s_val=0, pane_alive=pane_alive) == 0
|
||||
home=str(home), wait_s_val=0, pane_state=pane_state) == 0
|
||||
assert json.loads(same.read_text())["owner_pid"] == os.getpid()
|
||||
same.unlink()
|
||||
|
||||
@@ -1173,3 +1183,103 @@ def test_id_order_create_waits_for_orphan_herdr_session_of_a_killed_create(
|
||||
finally:
|
||||
holder.kill()
|
||||
holder.wait()
|
||||
|
||||
|
||||
def test_id_order_herdr_probes_stay_within_the_wait_cap(tmp_path, monkeypatch):
|
||||
"""PR #69 review r2 P2: probes of dead-owner markers get the time left until
|
||||
max(deadline, start + PROBE_FLOOR_S), shared by all markers, so a slow herdr
|
||||
cannot hold the wait (and its lock) past the cap; an expired probe is unknown."""
|
||||
from lib_py.agents import id_order
|
||||
|
||||
home = tmp_path / "home"
|
||||
ws = tmp_path / "ws"
|
||||
ws.mkdir(parents=True)
|
||||
yaml_path = tmp_path / "agent-sessions.yaml"
|
||||
yaml_path.write_text(yaml.safe_dump({"herdr_sessions": []}))
|
||||
monkeypatch.setenv("HOME", str(home))
|
||||
monkeypatch.setenv("AGENT_SESSIONS_YAML", str(yaml_path))
|
||||
dpath = id_order.order_dir(str(ws), "agy", home=str(home))
|
||||
dead = subprocess.Popen(["true"])
|
||||
dead.wait()
|
||||
for name in ("orphan-1-agy", "orphan-2-agy"):
|
||||
id_order.write_marker(dpath, name, dead.pid, str(ws), "agy", herdr_server="srv")
|
||||
|
||||
budgets = []
|
||||
|
||||
def slow_probe(server, session, timeout):
|
||||
budgets.append(timeout)
|
||||
if timeout <= 0:
|
||||
return None
|
||||
time.sleep(min(timeout, 5.0))
|
||||
return None
|
||||
|
||||
for cap in (0, 1, 3):
|
||||
budgets.clear()
|
||||
t0 = time.monotonic()
|
||||
rc = id_order.wait(str(ws), "agy", "new-agy", os.getpid(), yaml_path=str(yaml_path),
|
||||
home=str(home), wait_s_val=cap, poll=0.05, pane_state=slow_probe)
|
||||
elapsed = time.monotonic() - t0
|
||||
bound = max(cap, id_order.PROBE_FLOOR_S)
|
||||
assert rc == 1, cap
|
||||
assert elapsed < bound + 0.6, (cap, elapsed)
|
||||
assert budgets and max(budgets) <= bound + 0.01, (cap, budgets)
|
||||
assert len(list(Path(dpath).glob("orphan-*.json"))) == 2, cap
|
||||
|
||||
|
||||
def test_id_order_real_probe_against_mock_herdr_fails_closed(mam_sandbox, mock_herdr):
|
||||
"""PR #69 review r2 P1: lib.sh mam_id_order_wait with the real probe (sandbox
|
||||
copy, mock herdr). A listed orphan blocks; an unlisted one is pruned; a failed
|
||||
`agent list` keeps the marker and refuses; herdr's server_not_running prunes."""
|
||||
from lib_py.agents import id_order
|
||||
|
||||
ws = mam_sandbox / "probe-ws"
|
||||
ws.mkdir(parents=True, exist_ok=True)
|
||||
name_a, name_b = "probe-ws-creator-agy", "probe-ws-reviewer-agy"
|
||||
lib = mam_sandbox / ".agents" / "skills" / "lib.sh"
|
||||
dpath = id_order.order_dir(str(ws), "agy", home=str(mam_sandbox))
|
||||
marker_a = Path(dpath) / f"{name_a}.json"
|
||||
dead = subprocess.Popen(["true"])
|
||||
dead.wait()
|
||||
holder = subprocess.Popen(["sleep", "120"])
|
||||
|
||||
def set_mock(**kw):
|
||||
state = json.loads(Path(mock_herdr).read_text())
|
||||
state.pop("force_error_code", None)
|
||||
state["agents"].pop(name_a, None)
|
||||
state.update(kw)
|
||||
Path(mock_herdr).write_text(json.dumps(state))
|
||||
|
||||
def wait_b():
|
||||
for f in Path(dpath).glob(f"{name_b}.json"):
|
||||
f.unlink()
|
||||
id_order.write_marker(dpath, name_a, dead.pid, str(ws), "agy", herdr_server="probe-ws")
|
||||
return subprocess.run(
|
||||
["bash", "-c", 'source "$1" && mam_id_order_wait "$2" agy "$3"', "_", str(lib), str(ws), name_b],
|
||||
capture_output=True, text=True, cwd=str(mam_sandbox), timeout=120,
|
||||
env=dict(os.environ, MAM_ID_ORDER_WAIT_S="0", HERDR_SESSION_NAME="probe-ws"),
|
||||
)
|
||||
|
||||
try:
|
||||
_mock_herdr_agent(mock_herdr, name_a, ws, holder.pid, int(time.time()), pane_id="w1:p_a")
|
||||
res = wait_b()
|
||||
assert res.returncode == 1 and "its herdr session is alive" in res.stderr, res.stderr
|
||||
assert marker_a.exists()
|
||||
|
||||
set_mock(force_error_code="protocol_mismatch")
|
||||
res = wait_b()
|
||||
assert res.returncode == 1, res.stderr
|
||||
assert "herdr could not tell whether its session is gone" in res.stderr
|
||||
assert marker_a.exists()
|
||||
|
||||
set_mock(force_error_code="server_not_running")
|
||||
res = wait_b()
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert not marker_a.exists()
|
||||
|
||||
set_mock()
|
||||
res = wait_b()
|
||||
assert res.returncode == 0, res.stderr
|
||||
assert not marker_a.exists()
|
||||
finally:
|
||||
holder.kill()
|
||||
holder.wait()
|
||||
Reference in new issue
Block a user