2023-01-27 21:27:25 +00:00
|
|
|
'''
|
2026-09-26 00:27:46 +00:00
|
|
|
Detect a closed IPC transport during real-time streaming.
|
|
|
|
|
|
|
|
|
|
Detected closure must fail an unfinished context promptly, including
|
|
|
|
|
when the peer sent a stream stop first. Only the no-break example
|
|
|
|
|
simulates a user pressing Ctrl-C to cancel a healthy actor tree.
|
2023-01-27 21:27:25 +00:00
|
|
|
|
|
|
|
|
'''
|
2024-03-19 23:33:06 +00:00
|
|
|
from contextlib import asynccontextmanager as acm
|
2024-03-12 19:48:20 +00:00
|
|
|
|
2023-01-26 22:48:08 +00:00
|
|
|
from tractor import (
|
|
|
|
|
open_nursery,
|
|
|
|
|
context,
|
|
|
|
|
Context,
|
2024-03-12 19:48:20 +00:00
|
|
|
_testing,
|
2023-01-26 22:48:08 +00:00
|
|
|
)
|
2024-03-12 19:48:20 +00:00
|
|
|
import trio
|
2023-01-26 22:48:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@context
|
2023-01-27 21:27:25 +00:00
|
|
|
async def recv_and_spawn_net_killers(
|
2023-01-26 22:48:08 +00:00
|
|
|
ctx: Context,
|
2024-03-12 19:48:20 +00:00
|
|
|
break_ipc_after: bool|int = False,
|
|
|
|
|
pre_close: bool = False,
|
2023-01-26 22:48:08 +00:00
|
|
|
) -> None:
|
|
|
|
|
'''
|
2026-09-26 00:27:46 +00:00
|
|
|
Echo messages until a single, sequence-numbered IPC break.
|
|
|
|
|
|
|
|
|
|
Break before echoing the trigger so the caller is waiting for
|
|
|
|
|
that reply, rather than racing another send against peer close.
|
|
|
|
|
Keep this context unfinished: stream stop is not a final result.
|
2023-01-26 22:48:08 +00:00
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
await ctx.started()
|
2026-09-26 00:27:46 +00:00
|
|
|
async with ctx.open_stream() as stream:
|
2023-01-28 03:59:15 +00:00
|
|
|
async for i in stream:
|
|
|
|
|
if (
|
2023-01-28 21:44:35 +00:00
|
|
|
break_ipc_after
|
2024-03-12 19:48:20 +00:00
|
|
|
and
|
2024-03-19 23:33:06 +00:00
|
|
|
i >= break_ipc_after
|
2023-01-28 03:59:15 +00:00
|
|
|
):
|
2026-09-26 00:27:46 +00:00
|
|
|
if pre_close:
|
|
|
|
|
await stream.aclose()
|
|
|
|
|
# Protocol Stop closes the stream, not its IPC.
|
|
|
|
|
assert stream.closed
|
|
|
|
|
assert ctx.chan.connected()
|
|
|
|
|
await _testing.break_ipc(
|
|
|
|
|
stream=stream,
|
|
|
|
|
pre_close=False,
|
2024-03-12 19:48:20 +00:00
|
|
|
)
|
2026-09-26 00:27:46 +00:00
|
|
|
await trio.sleep_forever()
|
|
|
|
|
|
|
|
|
|
print(f'child echoing {i}')
|
|
|
|
|
await stream.send(i)
|
2024-03-19 23:33:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@acm
|
|
|
|
|
async def stuff_hangin_ctlc(timeout: float = 1) -> None:
|
2026-09-26 00:27:46 +00:00
|
|
|
'''
|
|
|
|
|
Simulate Ctrl-C when a healthy stream makes no progress.
|
2024-03-19 23:33:06 +00:00
|
|
|
|
2026-09-26 00:27:46 +00:00
|
|
|
Detected-fault runs use a failing deadline instead, so a timeout
|
|
|
|
|
cannot masquerade as successful transport-failure propagation.
|
|
|
|
|
|
|
|
|
|
'''
|
2024-03-19 23:33:06 +00:00
|
|
|
with trio.move_on_after(timeout) as cs:
|
|
|
|
|
yield timeout
|
|
|
|
|
|
|
|
|
|
if cs.cancelled_caught:
|
2026-09-26 00:27:46 +00:00
|
|
|
# Pretend to be a user seeing no streaming action,
|
|
|
|
|
# thinking it's a hang, and then hitting Ctrl-C.
|
2024-03-19 23:33:06 +00:00
|
|
|
print(
|
2026-09-26 00:27:46 +00:00
|
|
|
f'PARENT streaming stalled after {timeout}s\n'
|
|
|
|
|
f'\n'
|
|
|
|
|
f'MASHING Ctrl-C..!\n'
|
2024-03-19 23:33:06 +00:00
|
|
|
)
|
|
|
|
|
raise KeyboardInterrupt
|
2023-01-26 22:48:08 +00:00
|
|
|
|
|
|
|
|
|
2023-01-27 21:27:25 +00:00
|
|
|
async def main(
|
2023-01-27 22:02:36 +00:00
|
|
|
debug_mode: bool = False,
|
|
|
|
|
start_method: str = 'trio',
|
2024-03-12 19:48:20 +00:00
|
|
|
loglevel: str = 'cancel',
|
2023-01-28 21:44:35 +00:00
|
|
|
|
2026-09-26 00:27:46 +00:00
|
|
|
# By default the parent breaks first, if configured to break
|
|
|
|
|
# at all. Reverse the thresholds to make the child break first.
|
2024-03-12 19:48:20 +00:00
|
|
|
break_parent_ipc_after: int|bool = False,
|
|
|
|
|
break_child_ipc_after: int|bool = False,
|
|
|
|
|
pre_close: bool = False,
|
2025-04-04 04:05:55 +00:00
|
|
|
tpt_proto: str = 'tcp',
|
2026-09-26 00:27:46 +00:00
|
|
|
reg_addr: tuple[str, int|str]|None = None,
|
2023-01-27 21:27:25 +00:00
|
|
|
) -> None:
|
2026-09-26 00:27:46 +00:00
|
|
|
'''
|
|
|
|
|
Run a bounded close scenario or demonstrate Ctrl-C cancellation.
|
2023-01-26 22:48:08 +00:00
|
|
|
|
2026-09-26 00:27:46 +00:00
|
|
|
Tests pass their unique registrar address explicitly. Standalone
|
|
|
|
|
invocation retains the runtime's default address selection.
|
2023-01-27 21:27:25 +00:00
|
|
|
|
2026-09-26 00:27:46 +00:00
|
|
|
'''
|
|
|
|
|
registry_kwargs = {}
|
|
|
|
|
if reg_addr is not None:
|
|
|
|
|
registry_kwargs = {
|
|
|
|
|
'registry_addrs': [reg_addr],
|
|
|
|
|
'ensure_registry': True,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async with open_nursery(
|
|
|
|
|
start_method=start_method,
|
|
|
|
|
# Even with debugging enabled, broken IPC must not hang
|
|
|
|
|
# trying to engage a debugger through the closed channel.
|
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
|
loglevel=loglevel,
|
|
|
|
|
enable_transports=[tpt_proto],
|
|
|
|
|
**registry_kwargs,
|
|
|
|
|
) as an:
|
2023-01-28 03:59:15 +00:00
|
|
|
portal = await an.start_actor(
|
2026-09-26 00:27:46 +00:00
|
|
|
'chitty_hijo',
|
2023-01-26 22:48:08 +00:00
|
|
|
enable_modules=[__name__],
|
|
|
|
|
)
|
2026-09-26 00:27:46 +00:00
|
|
|
breaking = (
|
|
|
|
|
break_parent_ipc_after
|
|
|
|
|
or
|
|
|
|
|
break_child_ipc_after
|
|
|
|
|
)
|
|
|
|
|
async with portal.open_context(
|
|
|
|
|
recv_and_spawn_net_killers,
|
|
|
|
|
break_ipc_after=break_child_ipc_after,
|
|
|
|
|
pre_close=pre_close,
|
|
|
|
|
) as (ctx, sent):
|
|
|
|
|
# A transport failure is not a remote cancellation RPC;
|
|
|
|
|
# do not require ContextCancelled or invent a canceller.
|
|
|
|
|
with trio.fail_after(3):
|
|
|
|
|
async with ctx.open_stream() as stream:
|
|
|
|
|
for i in range(1000):
|
|
|
|
|
if (
|
|
|
|
|
break_parent_ipc_after
|
|
|
|
|
and
|
|
|
|
|
i >= break_parent_ipc_after
|
|
|
|
|
):
|
|
|
|
|
# TODO: other methods? See break_ipc().
|
|
|
|
|
# await ctx.chan.send(None)
|
|
|
|
|
socket = ctx.chan.transport.stream
|
|
|
|
|
# await socket.send_eof()
|
|
|
|
|
await socket.aclose()
|
|
|
|
|
assert not ctx.chan.connected()
|
|
|
|
|
await trio.sleep_forever()
|
2023-01-26 22:48:08 +00:00
|
|
|
|
2024-03-12 19:48:20 +00:00
|
|
|
await stream.send(i)
|
2026-09-26 00:27:46 +00:00
|
|
|
if breaking:
|
2024-03-12 19:48:20 +00:00
|
|
|
rx = await stream.receive()
|
2026-09-26 00:27:46 +00:00
|
|
|
else:
|
|
|
|
|
async with stuff_hangin_ctlc():
|
|
|
|
|
rx = await stream.receive()
|
|
|
|
|
assert rx == i
|
|
|
|
|
|
|
|
|
|
if breaking:
|
|
|
|
|
# open_stream() absorbs a peer's stop/EoC, but
|
|
|
|
|
# open_context() still needs a result. EOF must
|
|
|
|
|
# wake this wait without a later user SIGINT.
|
|
|
|
|
await ctx.wait_for_result()
|
|
|
|
|
raise AssertionError('broken ctx returned?')
|
|
|
|
|
|
|
|
|
|
raise KeyboardInterrupt
|
2023-01-26 22:48:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
trio.run(main)
|