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
|
||||
|
||||
|
||||
# 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(
|
||||
scope='module',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -327,6 +327,55 @@ def _reap_orphaned_subactors():
|
|||
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')
|
||||
def debug_mode(
|
||||
request: pytest.FixtureRequest,
|
||||
|
|
|
|||
Loading…
Reference in New Issue