tractor/tests/test_advanced_faults.py

372 lines
12 KiB
Python
Raw Normal View History

'''
Sketchy network blackoutz, ugly byzantine gens, puedes eschuchar la
cancelacion?..
'''
from contextlib import asynccontextmanager as acm
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
from types import ModuleType
import pytest
from _pytest.pathlib import import_path
import trio
import tractor
from tractor import (
Portal,
TransportClosed,
)
from tractor.runtime._supervise import ActorNursery
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
from tractor._testing import (
examples_dir,
break_ipc,
assert_child_reaped,
)
@pytest.fixture
def reaped_children(monkeypatch: pytest.MonkeyPatch):
'''
Retain spawned process handles and check synchronous reaping.
Snapshot each ActorNursery._children entry at publication, before
failure teardown removes it. Check both backend completion and
PID disappearance after trio.run(), without an external reaper.
'''
procs = []
start_actor = ActorNursery.start_actor
async def record_child(an, *args, **kwargs):
'''
Record only children spawned by this test's actor nursery.
'''
portal = await start_actor(an, *args, **kwargs)
procs.append(an._children[portal.channel.aid.uid][1])
return portal
monkeypatch.setattr(ActorNursery, 'start_actor', record_child)
yield procs
for proc in procs:
assert_child_reaped(proc)
@pytest.fixture
def fault_contexts(monkeypatch: pytest.MonkeyPatch):
'''
Observe fault state without altering cancellation or propagation.
Collect the entered `tractor.Context`s in a `list` delivered
to the consuming test. This allows introspecting a ctx's recorded
fault even if teardown incorrectly swallows that failure.
On teardown ensure transport-loss does not set
`ContextStatus._canceller`: connection loss alone supplies no
remote cancellation or task-error provenance. For the simulated
user-interrupt cases, ensure `Context.cancel_called` is True
because teardown requested cancellation of the remote task.
'''
contexts: list[tractor.Context] = []
open_context = Portal.open_context
@acm
async def record_context(portal, func, **kwargs):
'''
Hook into all `Portal.open_context() as pair` calls, stashing
their contexts in a `list` for consumption by dependent
tests for introspection.
'''
ctx: tractor.Context|None = None
try:
async with open_context(
portal,
func,
**kwargs,
) as (ctx, sent):
if func.__name__ in {
'recv_and_spawn_net_killers',
'break_ipc_after_started',
}:
contexts.append(ctx)
yield (ctx, sent)
finally:
if (
ctx in contexts
and
isinstance(ctx.maybe_error, TransportClosed)
):
# MsgStream closure is distinct from transport loss.
# After teardown, send prioritizes the ctx error,
# while receive retains the stream's terminal marker.
stream = ctx._stream
assert stream.closed
assert not ctx.chan.connected()
with pytest.raises(TransportClosed) as err:
await stream.send('after-close')
assert err.value is ctx.maybe_error
terminal = stream._eoc or stream._closed
assert isinstance(terminal, BaseException)
with pytest.raises(type(terminal)) as err:
await stream.receive()
assert err.value is terminal
monkeypatch.setattr(Portal, 'open_context', record_context)
yield contexts
for ctx in contexts:
if isinstance(ctx.maybe_error, TransportClosed):
assert ctx.canceller is None
elif isinstance(ctx._status._local_error, KeyboardInterrupt):
assert ctx.cancel_called
def assert_transport_closed(
exc: TransportClosed|BaseExceptionGroup,
local: bool,
) -> tuple[TransportClosed, ...]:
'''
Validate every failure leaf, including its socket-level source.
Parent close interrupts receive with ClosedResourceError.
Peer close can report clean EOF or a reset, depending on buffered
traffic. No arbitrary exception or remote cancellation qualifies.
'''
if isinstance(exc, BaseExceptionGroup):
return tuple(
fault
for leaf in exc.exceptions
for fault in assert_transport_closed(leaf, local=local)
)
assert isinstance(exc, TransportClosed)
cause = exc.__cause__
assert exc.src_exc is cause
if local:
assert isinstance(cause, trio.ClosedResourceError)
assert 'closed locally' in str(exc)
elif cause is None:
assert 'gracefully closed' in str(exc)
else:
assert 'closed by peer' in str(exc)
assert isinstance(
cause,
(ValueError, trio.BrokenResourceError,
ConnectionResetError),
)
if isinstance(cause, ValueError):
assert 'unclean EOF' in str(cause)
# TODO, should we expect a certain exc-message (per tpt) too?
# e.g. cause.args[0] == 'another task closed this fd'
return (exc,)
@pytest.mark.parametrize(
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
'pre_aclose_msgstream',
[False, True],
ids=['no_msgstream_aclose', 'pre_aclose_msgstream'],
)
@pytest.mark.parametrize(
'ipc_break',
[
# No breaks: retain the healthy Ctrl-C cancellation contract.
dict(break_parent_ipc_after=False,
break_child_ipc_after=False),
# Only parent breaks.
dict(break_parent_ipc_after=500,
break_child_ipc_after=False),
# Only child breaks.
dict(break_parent_ipc_after=False,
break_child_ipc_after=500),
# Both configured: parent breaks first.
dict(break_parent_ipc_after=500,
break_child_ipc_after=800),
# Both configured: child breaks first.
dict(break_parent_ipc_after=800,
break_child_ipc_after=500),
],
ids=[
'no_break',
'break_parent',
'break_child',
'break_both_parent_first',
'break_both_child_first',
],
)
def test_ipc_channel_break_during_stream(
debug_mode: bool,
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
loglevel: str,
spawn_backend: str,
ipc_break: dict,
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
pre_aclose_msgstream: bool,
tpt_proto: str,
reg_addr: tuple[str, int|str],
reaped_children,
fault_contexts: list[tractor.Context],
):
'''
Detect local/peer closure without waiting for a later Ctrl-C.
The old scenario required ContextCancelled and a hang even when
EOF had already notified the unfinished context. Sequence-number
thresholds choose the first breaker; the child closes before its
trigger reply, preventing the parent from advancing past it.
With pre-close, MsgStream absorbs the peer's `Stop`, but pending
Context.wait_for_result() must fail on transport loss. Check each
exception leaf and its source, bound teardown, and verify the
child is gone synchronously. No-break runs still raise synthetic
KeyboardInterrupt and reap the child through normal cancellation.
If Context._scope absorbs EOF-driven task cancellation, teardown
must raise the recorded fault despite a disconnected channel.
That path must not manufacture a cancel request or peer ack, and
must preserve the original TransportClosed object and its cause.
The fixture checks stream closure separately from transport
connectivity after unwinding, then retries send and receive to
verify their distinct error precedence and terminal identities.
'''
if spawn_backend != 'trio' and debug_mode:
pytest.skip('debug_mode only supported on trio spawner')
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
mod: ModuleType = import_path(
examples_dir() / 'advanced_faults'
/ 'ipc_failure_during_stream.py',
root=examples_dir(),
consider_namespace_packages=False,
)
parent = ipc_break['break_parent_ipc_after']
child = ipc_break['break_child_ipc_after']
breaking = parent or child
local = bool(
parent
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
and
(
(not child)
or
(parent < child)
)
)
Add (back) a `tractor._testing` sub-pkg Since importing from our top level `conftest.py` is not scaleable or as "future forward thinking" in terms of: - LoC-wise (it's only one file), - prevents "external" (aka non-test) example scripts from importing content easily, - seemingly(?) can't be used via abs-import if using a `[tool.pytest.ini_options]` in a `pyproject.toml` vs. a `pytest.ini`, see: https://docs.pytest.org/en/8.0.x/reference/customize.html#pyproject-toml) => Go back to having an internal "testing" pkg like `trio` (kinda) does. Deats: - move generic top level helpers into pkg-mod including the new `expect_ctxc()` (which i needed in the advanced faults testing script. - move `@tractor_test` into `._testing.pytest` sub-mod. - adjust all the helper imports to be a `from tractor._testing import <..>` Rework `test_ipc_channel_break_during_stream()` and backing script: - make test(s) pull `debug_mode` from new fixture (which is now controlled manually from `--tpdb` flag) and drop the previous parametrized input. - update logic in ^ test for "which-side-fails" cases to better match recently updated/stricter cancel/failure semantics in terms of `ClosedResouruceError` vs. `EndOfChannel` expectations. - handle `ExceptionGroup`s with expected embedded errors in test. - better pendantics around whether to expect a user simulated KBI. - for `examples/advanced_faults/ipc_failure_during_stream.py` script: - generalize ipc breakage in new `break_ipc()` with support for diff internal `trio` methods and a #TODO for future disti frameworks - only make one sub-actor task break and the other just stream. - use new `._testing.expect_ctxc()` around ctx block. - add a bit of exception handling with `print()`s around ctxc (unused except if 'msg' break method is set) and eoc cases. - don't break parent side ipc in loop any more then once after first break, checked via flag var. - add a `pre_close: bool` flag to control whether `MsgStreama.aclose()` is called *before* any ipc breakage method. Still TODO: - drop `pytest.ini` and add the alt section to `pyproject.py`. -> currently can't get `--rootdir=` opt to work.. not showing in console header. -> ^ also breaks on 'tests' `enable_modules` imports in subactors during discovery tests?
2024-03-12 19:48:20 +00:00
async def run():
'''
Bound the example, including actor nursery teardown.
'''
with trio.fail_after(10):
await mod.main(
debug_mode=debug_mode,
start_method=spawn_backend,
loglevel=loglevel,
pre_close=pre_aclose_msgstream,
tpt_proto=tpt_proto,
reg_addr=reg_addr,
**ipc_break,
)
if not breaking:
with pytest.raises(KeyboardInterrupt):
trio.run(run)
else:
with pytest.raises((TransportClosed, ExceptionGroup)) as err:
trio.run(run)
ctx = fault_contexts[0]
pytest.fail(
'Transport failure was swallowed:\n'
f'{ctx.repr_outcome()}\n'
f'error={ctx.maybe_error!r}'
)
faults = assert_transport_closed(err.value, local=local)
ctx = fault_contexts[0]
assert any(fault is ctx.maybe_error for fault in faults)
if ctx._scope.cancelled_caught:
error = ctx._status._remote_error
assert any(fault is error for fault in faults)
assert ctx._status._local_error is error
assert not ctx.cancel_called
assert not ctx.cancel_acked
assert len(reaped_children) == len(fault_contexts) == 1
@tractor.context
async def break_ipc_after_started(
ctx: tractor.Context,
) -> None:
'''
Wait for the caller's stream barrier, then close unfinished IPC.
'''
await ctx.started()
async with ctx.open_stream() as stream:
assert await stream.receive() == 'ready'
# TODO: make a test which verifies the error
# for this, i.e. raises a `MsgTypeError`
# await ctx.chan.send(None)
await break_ipc(stream=stream, pre_close=True)
await trio.sleep_forever()
def test_stream_closed_right_after_ipc_break_and_zombie_lord_engages(
reg_addr: tuple[str, int|str],
tpt_proto: str,
spawn_backend: str,
loglevel: str,
reaped_children,
fault_contexts,
):
'''
Reap a child after detected close without a later user SIGINT.
Previously the parent slept, timed out its context, then raised
KeyboardInterrupt to engage process supervision ("zombie lord").
EOF now notifies the unfinished context immediately. A ready
message ensures both streams exist before the child sends `Stop`
and closes IPC; the caller then waits for the missing result.
A bounded TransportClosed exit proves fault-driven teardown, and
reaped_children checks process completion and PID disappearance
synchronously. Timer-driven cancellation cannot pass this test.
'''
async def main():
'''
Await transport failure and bound the entire nursery exit.
'''
with trio.fail_after(10):
async with tractor.open_nursery(
registry_addrs=[reg_addr],
ensure_registry=True,
enable_transports=[tpt_proto],
start_method=spawn_backend,
loglevel=loglevel,
) as an:
portal = await an.start_actor(
'ipc_breaker',
enable_modules=[__name__],
)
async with portal.open_context(
break_ipc_after_started,
) as (ctx, sent):
async with ctx.open_stream() as stream:
await stream.send('ready')
await stream.receive()
await ctx.wait_for_result()
pytest.fail('broken context returned a result')
with pytest.raises((TransportClosed, ExceptionGroup)) as err:
trio.run(main)
faults = assert_transport_closed(err.value, local=False)
ctx = fault_contexts[0]
assert any(fault is ctx.maybe_error for fault in faults)
assert len(reaped_children) == len(fault_contexts) == 1