From 97574f6e0531db40b0d06fcd5125013072ba940e Mon Sep 17 00:00:00 2001 From: Vasily Ilin Date: Sat, 14 Nov 2026 13:16:20 -0800 Subject: [PATCH] Treat a process reaped mid-read as gone in the build-time watcher Reading /proc/PID/stat after the process is reaped raises ProcessLookupError (ESRCH), not FileNotFoundError, so watch() reported 'Cannot monitor Lean build processes' (exit 125) and failed the build. Seen as a flaky test_watch_prunes_exited_process_handles on #817. The gate is unchanged: a process that has exited is not compiling. Co-Authored-By: Claude Opus 5.5 --- python/lean_pool/build_time_limits.py | 5 +++-- python/tests/test_build_time_limits.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/python/lean_pool/build_time_limits.py b/python/lean_pool/build_time_limits.py index 26f964ec44..43be062b24 100644 --- a/python/lean_pool/build_time_limits.py +++ b/python/lean_pool/build_time_limits.py @@ -191,7 +191,8 @@ def _process_identity(pid: int) -> tuple[int, int, int] | None: """Read kernel start ticks, current parent and group for a candidate PID.""" try: stat = Path(f"/proc/{pid}/stat").read_text() - except FileNotFoundError: + except (FileNotFoundError, ProcessLookupError): + # ProcessLookupError: the process was reaped between open and read. return None fields = stat.rsplit(")", 1)[1].split() return int(fields[19]), int(fields[1]), int(fields[2]) @@ -322,7 +323,7 @@ def _process_state(pid: int) -> str | None: """Read the leader state without reaping its PID or releasing its group ID.""" try: stat = Path(f"/proc/{pid}/stat").read_text() - except FileNotFoundError: + except (FileNotFoundError, ProcessLookupError): return None return stat.rsplit(")", 1)[1].split()[0] diff --git a/python/tests/test_build_time_limits.py b/python/tests/test_build_time_limits.py index 923272a452..227c492a87 100644 --- a/python/tests/test_build_time_limits.py +++ b/python/tests/test_build_time_limits.py @@ -784,3 +784,14 @@ def test_workflow_rebuilds_changed_modules_after_all_compiled_restores() -> None assert restore_index < index, (name, step.get("name")) if "-m lean_pool.queue_build" in run and "--receipts" in run: assert restore_index > index + + +@pytest.mark.parametrize("reader", [limits._process_identity, limits._process_state]) +def test_a_process_reaped_mid_read_counts_as_gone(monkeypatch, reader) -> None: + """Reading /proc/PID/stat of a just-reaped process raises ESRCH, not ENOENT.""" + + def reaped(self, *args, **kwargs): + raise ProcessLookupError(3, "No such process") + + monkeypatch.setattr(limits.Path, "read_text", reaped) + assert reader(os.getpid()) is None