Add opt-in `reap_subactors_per_test` fixture
Function-scoped, NON-autouse zombie-subactor reaper for modules whose teardown is known-leaky enough to cascade- fail every following test in a session. Sibling to the autouse session-scoped `_reap_orphaned_subactors`. The session-scoped one fires at session end — too late to save tests that follow a hung/leaky test in the suite. The new fixture, opted into via `pytestmark = pytest.mark.usefixtures(...)`, runs between tests in a problem-module so a leftover subactor from test N can't squat on registrar ports / UDS paths / shm segments needed by tests N+1, N+2, ... Intentionally NOT autouse — the fixture's presence on a module signals "this module's teardown leaks; please root-cause instead of relying forever on cleanup". A visibility-vs-convenience trade picked in favor of the former. Apply to `tests/test_infected_asyncio.py` since both recent full-suite runs (parallel-tpt-proto + TCP-only) showed the cascade originating in this file's KBI- and SIGINT-flavored tests under `main_thread_forkserver`. Module-comment names the specific offenders so future de-flake work has a starting point. (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-codesubint_forkserver_backend
parent
7c5dd4d033
commit
b376eb0332
|
|
@ -32,6 +32,22 @@ from tractor.trionics import BroadcastReceiver
|
||||||
from tractor._testing import expect_ctxc
|
from tractor._testing import expect_ctxc
|
||||||
|
|
||||||
|
|
||||||
|
# Per-test zombie-subactor reaper. Opt-in (NOT autouse) —
|
||||||
|
# see `tractor._testing.pytest.reap_subactors_per_test`'s
|
||||||
|
# docstring for the full rationale. This module specifically
|
||||||
|
# needs it because tests like
|
||||||
|
# `test_echoserver_detailed_mechanics[KeyboardInterrupt]`
|
||||||
|
# and the `test_sigint_closes_lifetime_stack[*]` matrix have
|
||||||
|
# been observed to hang past pytest's wall-clock under
|
||||||
|
# `main_thread_forkserver`, leaving subactor forks that
|
||||||
|
# squat on registrar resources and cascade-fail every
|
||||||
|
# subsequent test (`test_inter_peer_cancellation`,
|
||||||
|
# `test_legacy_one_way_streaming`, etc.).
|
||||||
|
pytestmark = pytest.mark.usefixtures(
|
||||||
|
'reap_subactors_per_test',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
scope='module',
|
scope='module',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -327,6 +327,55 @@ def _reap_orphaned_subactors():
|
||||||
reap(pids, grace=3.0)
|
reap(pids, grace=3.0)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def reap_subactors_per_test():
|
||||||
|
'''
|
||||||
|
Per-test (function-scoped) zombie-subactor reaper —
|
||||||
|
**opt-in**, NOT autouse.
|
||||||
|
|
||||||
|
When a test's teardown fails to fully cancel its actor
|
||||||
|
tree (e.g. an asyncio cancel-cascade times out under
|
||||||
|
`main_thread_forkserver`, pytest hits its 200s wall-
|
||||||
|
clock and abandons), the leftover subactor lingers as a
|
||||||
|
direct child of `pytest` and squats on whatever
|
||||||
|
registrar port / UDS path / shm segment it had bound.
|
||||||
|
Subsequent tests trying to allocate the same resource
|
||||||
|
fail — and with backends that bind a session-shared
|
||||||
|
`reg_addr`, that means EVERY following test in the
|
||||||
|
suite cascades. The session-scoped sibling
|
||||||
|
(`_reap_orphaned_subactors`) only kicks in at session
|
||||||
|
end which is too late to save the cascade.
|
||||||
|
|
||||||
|
Apply at module-level on the topically-problematic
|
||||||
|
test files via:
|
||||||
|
|
||||||
|
```python
|
||||||
|
pytestmark = pytest.mark.usefixtures(
|
||||||
|
'reap_subactors_per_test',
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or per-test via the same `usefixtures` mark on a
|
||||||
|
specific function. Intentionally NOT autouse so the
|
||||||
|
fixture's presence on a module signals "this module's
|
||||||
|
teardown is known-leaky enough to contaminate
|
||||||
|
siblings"; the visibility helps future-us track down
|
||||||
|
root causes rather than burying them under blanket
|
||||||
|
cleanup.
|
||||||
|
|
||||||
|
'''
|
||||||
|
import os
|
||||||
|
parent_pid: int = os.getpid()
|
||||||
|
yield
|
||||||
|
from tractor._testing._reap import (
|
||||||
|
find_descendants,
|
||||||
|
reap,
|
||||||
|
)
|
||||||
|
pids: list[int] = find_descendants(parent_pid)
|
||||||
|
if pids:
|
||||||
|
reap(pids, grace=3.0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def debug_mode(
|
def debug_mode(
|
||||||
request: pytest.FixtureRequest,
|
request: pytest.FixtureRequest,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue