194 lines
5.2 KiB
Python
194 lines
5.2 KiB
Python
'''
|
|
Sketchy network blackoutz, ugly byzantine gens, puedes eschuchar la
|
|
cancelacion?..
|
|
|
|
'''
|
|
from functools import partial
|
|
|
|
import pytest
|
|
from _pytest.pathlib import import_path
|
|
import trio
|
|
import tractor
|
|
|
|
from conftest import (
|
|
examples_dir,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'debug_mode',
|
|
[False, True],
|
|
ids=['no_debug_mode', 'debug_mode'],
|
|
)
|
|
@pytest.mark.parametrize(
|
|
'ipc_break',
|
|
[
|
|
# no breaks
|
|
{
|
|
'break_parent_ipc_after': False,
|
|
'break_child_ipc_after': False,
|
|
},
|
|
|
|
# only parent breaks
|
|
{
|
|
'break_parent_ipc_after': 500,
|
|
'break_child_ipc_after': False,
|
|
},
|
|
|
|
# only child breaks
|
|
{
|
|
'break_parent_ipc_after': False,
|
|
'break_child_ipc_after': 500,
|
|
},
|
|
|
|
# both: break parent first
|
|
{
|
|
'break_parent_ipc_after': 500,
|
|
'break_child_ipc_after': 800,
|
|
},
|
|
# both: break child first
|
|
{
|
|
'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,
|
|
spawn_backend: str,
|
|
ipc_break: dict | None,
|
|
):
|
|
'''
|
|
Ensure we can have an IPC channel break its connection during
|
|
streaming and it's still possible for the (simulated) user to kill
|
|
the actor tree using SIGINT.
|
|
|
|
We also verify the type of connection error expected in the parent
|
|
depending on which side if the IPC breaks first.
|
|
|
|
'''
|
|
if spawn_backend != 'trio':
|
|
if debug_mode:
|
|
pytest.skip('`debug_mode` only supported on `trio` spawner')
|
|
|
|
# non-`trio` spawners should never hit the hang condition that
|
|
# requires the user to do ctl-c to cancel the actor tree.
|
|
expect_final_exc = trio.ClosedResourceError
|
|
|
|
mod = import_path(
|
|
examples_dir() / 'advanced_faults' / 'ipc_failure_during_stream.py',
|
|
root=examples_dir(),
|
|
)
|
|
|
|
expect_final_exc = KeyboardInterrupt
|
|
|
|
# when ONLY the child breaks we expect the parent to get a closed
|
|
# resource error on the next `MsgStream.receive()` and then fail out
|
|
# and cancel the child from there.
|
|
if (
|
|
|
|
# only child breaks
|
|
(
|
|
ipc_break['break_child_ipc_after']
|
|
and ipc_break['break_parent_ipc_after'] is False
|
|
)
|
|
|
|
# both break but, parent breaks first
|
|
or (
|
|
ipc_break['break_child_ipc_after'] is not False
|
|
and (
|
|
ipc_break['break_parent_ipc_after']
|
|
> ipc_break['break_child_ipc_after']
|
|
)
|
|
)
|
|
|
|
):
|
|
expect_final_exc = trio.ClosedResourceError
|
|
|
|
# when the parent IPC side dies (even if the child's does as well
|
|
# but the child fails BEFORE the parent) we expect the channel to be
|
|
# sent a stop msg from the child at some point which will signal the
|
|
# parent that the stream has been terminated.
|
|
# NOTE: when the parent breaks "after" the child you get this same
|
|
# case as well, the child breaks the IPC channel with a stop msg
|
|
# before any closure takes place.
|
|
elif (
|
|
# only parent breaks
|
|
(
|
|
ipc_break['break_parent_ipc_after']
|
|
and ipc_break['break_child_ipc_after'] is False
|
|
)
|
|
|
|
# both break but, child breaks first
|
|
or (
|
|
ipc_break['break_parent_ipc_after'] is not False
|
|
and (
|
|
ipc_break['break_child_ipc_after']
|
|
> ipc_break['break_parent_ipc_after']
|
|
)
|
|
)
|
|
):
|
|
expect_final_exc = trio.EndOfChannel
|
|
|
|
with pytest.raises(expect_final_exc):
|
|
trio.run(
|
|
partial(
|
|
mod.main,
|
|
debug_mode=debug_mode,
|
|
start_method=spawn_backend,
|
|
**ipc_break,
|
|
)
|
|
)
|
|
|
|
|
|
@tractor.context
|
|
async def break_ipc_after_started(
|
|
ctx: tractor.Context,
|
|
) -> None:
|
|
await ctx.started()
|
|
async with ctx.open_stream() as stream:
|
|
await stream.aclose()
|
|
await trio.sleep(0.2)
|
|
await ctx.chan.send(None)
|
|
print('child broke IPC and terminating')
|
|
|
|
|
|
def test_stream_closed_right_after_ipc_break_and_zombie_lord_engages():
|
|
'''
|
|
Verify that is a subactor's IPC goes down just after bringing up a stream
|
|
the parent can trigger a SIGINT and the child will be reaped out-of-IPC by
|
|
the localhost process supervision machinery: aka "zombie lord".
|
|
|
|
'''
|
|
async def main():
|
|
async with tractor.open_nursery() as n:
|
|
portal = await n.start_actor(
|
|
'ipc_breaker',
|
|
enable_modules=[__name__],
|
|
)
|
|
|
|
with trio.move_on_after(1):
|
|
async with (
|
|
portal.open_context(
|
|
break_ipc_after_started
|
|
) as (ctx, sent),
|
|
):
|
|
async with ctx.open_stream():
|
|
await trio.sleep(0.5)
|
|
|
|
print('parent waiting on context')
|
|
|
|
print('parent exited context')
|
|
raise KeyboardInterrupt
|
|
|
|
with pytest.raises(KeyboardInterrupt):
|
|
trio.run(main)
|