mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
fix(runtime): isolate historical job lookup from PID reuse
This commit is contained in:
@@ -102,7 +102,11 @@ restoration during cancellation.
|
||||
## Job history and continuations
|
||||
|
||||
`peek()` and resolution do not refresh or reap jobs. Output refresh reconciles
|
||||
only the selected job, including its owned subprocess handle. Stop/output/ack
|
||||
only the selected job. It polls a cached subprocess handle only while the
|
||||
selected record is running and its frozen start token still verifies as owned;
|
||||
historical or unverifiable identities cannot poll a replacement handle under
|
||||
the same numeric PID. Global service refresh still reaps completed handles.
|
||||
Stop/output/ack
|
||||
require the caller's exact expected resource and revalidate linkage. Results
|
||||
can update only an explicit result-field whitelist, never identity, owner,
|
||||
generation, receipt, PID, command, path or authority fields.
|
||||
@@ -198,6 +202,11 @@ the integrated gate spans the 145-file manifest. Validation used
|
||||
`/tmp/odysseus-wave3-validation/bin/python` with functional bubblewrap.
|
||||
Compileall, diff whitespace, conflict-marker and unmerged-index gates passed.
|
||||
The post-commit integrated result is recorded in the final checkpoint report.
|
||||
Final adversarial review found a numeric-PID-only cached-handle lookup in that
|
||||
commit. A follow-up patch adds frozen-token validation and four PID-reuse/
|
||||
unverifiable history regressions, plus a service-cleanup regression. The patched
|
||||
focused gate passes 392 tests; the patched 145-file integrated gate passes 3369
|
||||
tests, with the same 3 platform skips and 2 existing xfails. Static gates pass.
|
||||
Platform skips remain
|
||||
explicit: `/tmp` is not a symlink, RLIMIT_AS can be lowered on this host, and the
|
||||
Windows-specific Ollama startup guard is not applicable on Linux. No missing
|
||||
|
||||
+10
-2
@@ -231,8 +231,16 @@ def refresh(job_id=None) -> Dict[str, Dict[str, Any]]:
|
||||
timeout). Idempotent — safe to call from a poll loop. Returns the store."""
|
||||
jobs = _load()
|
||||
for pid, proc in list(_LIVE_PROCS.items()):
|
||||
if job_id is not None and pid != jobs.get(job_id, {}).get("pid"):
|
||||
continue
|
||||
if job_id is not None:
|
||||
selected = jobs.get(job_id, {})
|
||||
# Historical numeric PIDs can name a replacement child's cached
|
||||
# handle. Targeted reads may poll only the frozen live incarnation;
|
||||
# independent service maintenance may still reap completed handles.
|
||||
if (selected.get("status") != "running"
|
||||
or pid != selected.get("pid")
|
||||
or process_ownership.verify(pid, selected.get("start_token"))
|
||||
!= process_ownership.OWNED):
|
||||
continue
|
||||
if proc.poll() is not None:
|
||||
_LIVE_PROCS.pop(pid, None)
|
||||
changed = False
|
||||
|
||||
@@ -215,6 +215,38 @@ def test_target_lookup_does_not_wait_on_unrelated_live_handle(store, monkeypatch
|
||||
bg_jobs.get("job", expected=resource)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("status", ["done", "running"])
|
||||
@pytest.mark.parametrize("verdict", [process_ownership.FOREIGN, process_ownership.UNVERIFIABLE])
|
||||
def test_historical_lookup_does_not_reap_reused_pid_handle(store, monkeypatch, status, verdict):
|
||||
resource, rec = seed(store, status=status)
|
||||
from pathlib import Path
|
||||
Path(rec["exit_path"]).write_text("0")
|
||||
Path(rec["result_path"]).write_text(json.dumps({
|
||||
"resource_identity": resource.to_dict(),
|
||||
"containment": {"id": resource.containment_id},
|
||||
}))
|
||||
monkeypatch.setattr(process_ownership, "verify", lambda *a: verdict)
|
||||
|
||||
class ReplacementProcess:
|
||||
def poll(self):
|
||||
pytest.fail("Historical lookup reaped the replacement incarnation")
|
||||
|
||||
replacement = ReplacementProcess()
|
||||
monkeypatch.setattr(bg_jobs, "_LIVE_PROCS", {rec["pid"]: replacement})
|
||||
assert bg_jobs.get("job", expected=resource)["status"] == "done"
|
||||
assert bg_jobs._LIVE_PROCS[rec["pid"]] is replacement
|
||||
|
||||
|
||||
def test_service_refresh_still_reaps_finished_handles(store, monkeypatch):
|
||||
class FinishedProcess:
|
||||
def poll(self):
|
||||
return 0
|
||||
|
||||
monkeypatch.setattr(bg_jobs, "_LIVE_PROCS", {4321: FinishedProcess()})
|
||||
bg_jobs.refresh()
|
||||
assert bg_jobs._LIVE_PROCS == {}
|
||||
|
||||
|
||||
def test_completed_result_outlives_lifecycle_receipt_without_signalling(store, monkeypatch):
|
||||
resource, rec = seed(store, status="done")
|
||||
from pathlib import Path
|
||||
|
||||
Reference in New Issue
Block a user