Mk per-test reap fixtures opt-in

Rename `_track_orphaned_uds_per_test` and
`_detect_runaway_subactors_per_test` to public names (drop `_` prefix),
drop `autouse=True`. Tests that need per-test reap blame now opt in via
`pytestmark = pytest.mark.usefixtures(...)`.

Also,
- reduce `sample_interval` from 0.5 -> 0.05s so the CPU probe is cheaper
  per pid.
- add empty-`only_pids` fast-path in `find_runaway_subactors` to skip
  psutil import when no descendants were spawned.
- extract `new_pids` intermediate var for clarity.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
subint_forkserver_backend
Gud Boi 2026-05-06 13:29:49 -04:00
parent c4082be876
commit e4953851de
1 changed files with 58 additions and 15 deletions

View File

@ -222,7 +222,7 @@ def find_runaway_subactors(
parent_pid: int, parent_pid: int,
*, *,
cpu_threshold: float = 95.0, cpu_threshold: float = 95.0,
sample_interval: float = 0.5, sample_interval: float = 0.05,
only_pids: set[int]|None = None, only_pids: set[int]|None = None,
) -> list[tuple[int, float, str]]: ) -> list[tuple[int, float, str]]:
''' '''
@ -237,16 +237,30 @@ def find_runaway_subactors(
`cpu_percent(interval=sample_interval)` is the `cpu_percent(interval=sample_interval)` is the
canonical psutil API for a "what %CPU is this proc canonical psutil API for a "what %CPU is this proc
using NOW" answer — it samples twice with a delta to using NOW" answer — it samples twice with a delta to
compute true utilization. compute true utilization. Default `sample_interval`
of 50ms is enough to register a sustained C-level
tight-loop at ~100% but cheap enough to run as part
of an autouse per-test fixture without dominating
suite wall-clock. Sub-50ms transient bursts are NOT
the bug class we're hunting (those are normal Python
work) so the lost sensitivity is fine.
`only_pids` filters to a specific pre-snapshotted set `only_pids` filters to a specific pre-snapshotted set
(e.g. "pids spawned during this test only"); when (e.g. "pids spawned during this test only"); when
`None`, all live descendants are checked. `None`, all live descendants are checked. Empty
`only_pids` returns `[]` IMMEDIATELY fast path for
tests that didn't spawn anything new.
Returns `[]` when `psutil` isn't installed or no Returns `[]` when `psutil` isn't installed or no
descendants match. descendants match.
''' '''
# Fast-path: caller passed empty `only_pids` —
# nothing to sample. Avoids the psutil import + /proc
# walk for tests that didn't spawn descendants.
if only_pids is not None and not only_pids:
return []
try: try:
import psutil import psutil
except ImportError: except ImportError:
@ -718,12 +732,25 @@ def _reap_orphaned_subactors():
@pytest.fixture( @pytest.fixture(
scope='function', scope='function',
autouse=True,
) )
def _track_orphaned_uds_per_test(): def track_orphaned_uds_per_test():
''' '''
Per-test (function-scoped) autouse UDS sock-file leak Per-test (function-scoped) UDS sock-file leak
detector + reaper. detector + reaper. **Opt-in**, NOT autouse.
Apply at module level on UDS-heavy test files via:
pytestmark = pytest.mark.usefixtures(
'track_orphaned_uds_per_test',
)
The session-end `_reap_orphaned_subactors` fixture
is the always-on safety net that catches leaks at
suite teardown; this per-test fixture is for the
smaller set of modules where blame attribution per
test matters (i.e. modules with a HISTORY of leaky
teardown that flakifies sibling tests via
sock-file rebind races).
Snapshots `${XDG_RUNTIME_DIR}/tractor/` before and Snapshots `${XDG_RUNTIME_DIR}/tractor/` before and
after each test; any `<name>@<pid>.sock` files after each test; any `<name>@<pid>.sock` files
@ -797,12 +824,18 @@ def _track_orphaned_uds_per_test():
@pytest.fixture( @pytest.fixture(
scope='function', scope='function',
autouse=True,
) )
def _detect_runaway_subactors_per_test(): def detect_runaway_subactors_per_test():
''' '''
Per-test (function-scoped) autouse runaway-subactor Per-test (function-scoped) runaway-subactor detector.
detector. **Opt-in**, NOT autouse.
Apply at module level on cancellation-cascade-heavy
test files via:
pytestmark = pytest.mark.usefixtures(
'detect_runaway_subactors_per_test',
)
Snapshots descendant pids before+after each test; Snapshots descendant pids before+after each test;
for any pid spawned during the test that's still for any pid spawned during the test that's still
@ -826,7 +859,7 @@ def _detect_runaway_subactors_per_test():
as needed. as needed.
Cost: one extra `os.listdir('/proc')` snapshot Cost: one extra `os.listdir('/proc')` snapshot
pre-test, one snapshot + N×`psutil.cpu_percent(0.5)` pre-test, one snapshot + N×`psutil.cpu_percent(0.05)`
post-test (only when there ARE new descendants post-test (only when there ARE new descendants
most tests don't trigger any sampling). Skips most tests don't trigger any sampling). Skips
silently when `psutil` isn't installed. silently when `psutil` isn't installed.
@ -876,6 +909,11 @@ def _detect_runaway_subactors_per_test():
# subactor is still burning CPU when the next test # subactor is still burning CPU when the next test
# starts. The warning comes ONE TEST LATE which is # starts. The warning comes ONE TEST LATE which is
# imperfect but better than silence. # imperfect but better than silence.
#
# NB, in the typical clean case `pre_existing` is
# empty (no test descendants leftover) and the
# `find_runaway_subactors` call short-circuits
# without even loading `psutil`.
pre_existing: set[int] = set(find_descendants(parent_pid)) pre_existing: set[int] = set(find_descendants(parent_pid))
pre_runaways: list[tuple[int, float, str]] = ( pre_runaways: list[tuple[int, float, str]] = (
find_runaway_subactors( find_runaway_subactors(
@ -899,12 +937,17 @@ def _detect_runaway_subactors_per_test():
# `ai/conc-anal/trio_wakeup_socketpair_busy_loop_under_fork_issue.md` # `ai/conc-anal/trio_wakeup_socketpair_busy_loop_under_fork_issue.md`
# for the canonical fork-spawn forkserver-worker # for the canonical fork-spawn forkserver-worker
# post-fork-close gap). # post-fork-close gap).
#
# `new_pids` is typically empty for tests that
# cleanly tore down their subactor tree; the call
# short-circuits before any `psutil` work.
new_pids: set[int] = (
set(find_descendants(parent_pid)) - pre_existing
)
post_runaways: list[tuple[int, float, str]] = ( post_runaways: list[tuple[int, float, str]] = (
find_runaway_subactors( find_runaway_subactors(
parent_pid, parent_pid,
only_pids=set( only_pids=new_pids,
find_descendants(parent_pid)
) - pre_existing,
) )
) )
if post_runaways: if post_runaways: