Compare commits
109 Commits
249e99a2d9
...
b8019f90ec
Author | SHA1 | Date |
---|---|---|
|
b8019f90ec | |
|
f6ba50979b | |
|
b3d348ee6a | |
|
d432e2e245 | |
|
ab013e3069 | |
|
6344f9cdb7 | |
|
82b5bd52c8 | |
|
dc806b8aba | |
|
5ab642bdf0 | |
|
ed18ecd064 | |
|
cec0282953 | |
|
25c5847f2e | |
|
ba793fadd9 | |
|
d17864a432 | |
|
6c361a9564 | |
|
34ca7429c7 | |
|
c9a55c2d46 | |
|
548855b4f5 | |
|
5322861d6d | |
|
46a2fa7074 | |
|
bfe5b2dde6 | |
|
a9f06df3fb | |
|
ee32bc433c | |
|
561954594e | |
|
28a6354e81 | |
|
d1599449e7 | |
|
2d27c94dec | |
|
6e4c76245b | |
|
a6f599901c | |
|
0fafd25f0d | |
|
b74e93ee55 | |
|
961504b657 | |
|
bd148300c5 | |
|
4a7491bda4 | |
|
62415518fc | |
|
5c7d930a9a | |
|
c46986504d | |
|
e05a4d3cac | |
|
a9aa5ec04e | |
|
5021514a6a | |
|
79f502034f | |
|
331921f612 | |
|
df0d00abf4 | |
|
a72d1e6c48 | |
|
5931c59aef | |
|
ba08052ddf | |
|
00112edd58 | |
|
1d706bddda | |
|
3c30c559d5 | |
|
599020c2c5 | |
|
50f6543ee7 | |
|
c0854fd221 | |
|
e875b62869 | |
|
3ab7498893 | |
|
dd041b0a01 | |
|
4e252526b5 | |
|
4ba3590450 | |
|
f1ff79a4e6 | |
|
70664b98de | |
|
1c425cbd22 | |
|
edc2211444 | |
|
b05abea51e | |
|
88c1c083bd | |
|
b096867d40 | |
|
a3c9822602 | |
|
e3a542f2b5 | |
|
0ffcea1033 | |
|
a7bdf0486c | |
|
d2ac9ecf95 | |
|
dcb1062bb8 | |
|
05d865c0f1 | |
|
8218f0f51f | |
|
8f19f5d3a8 | |
|
64c27a914b | |
|
d9c8d543b3 | |
|
048b154f00 | |
|
88828e9f99 | |
|
25ff195c17 | |
|
f60cc646ff | |
|
a2b754b5f5 | |
|
5e13588aed | |
|
0a56f40bab | |
|
f776c47cb4 | |
|
7f584d4f54 | |
|
d650dda0fa | |
|
f6598e8400 | |
|
59822ff093 | |
|
ca427aec7e | |
|
f53aa992af | |
|
69e0afccf0 | |
|
e275c49b23 | |
|
48fbf38c1d | |
|
defd6e28d2 | |
|
414b0e2bae | |
|
d34fb54f7c | |
|
5d87f63377 | |
|
0ca3d50602 | |
|
8880a80e3e | |
|
7be713ee1e | |
|
4bd8211abb | |
|
a23a98886c | |
|
31544c862c | |
|
7d320c4e1e | |
|
38944ad1d2 | |
|
9260909fe1 | |
|
c00b3c86ea | |
|
808a336508 | |
|
679d999185 | |
|
a8428d7de3 |
|
@ -16,6 +16,7 @@ from tractor import (
|
||||||
ContextCancelled,
|
ContextCancelled,
|
||||||
MsgStream,
|
MsgStream,
|
||||||
_testing,
|
_testing,
|
||||||
|
trionics,
|
||||||
)
|
)
|
||||||
import trio
|
import trio
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -62,9 +63,8 @@ async def recv_and_spawn_net_killers(
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
async with (
|
async with (
|
||||||
ctx.open_stream() as stream,
|
ctx.open_stream() as stream,
|
||||||
trio.open_nursery(
|
trionics.collapse_eg(),
|
||||||
strict_exception_groups=False,
|
trio.open_nursery() as tn,
|
||||||
) as tn,
|
|
||||||
):
|
):
|
||||||
async for i in stream:
|
async for i in stream:
|
||||||
print(f'child echoing {i}')
|
print(f'child echoing {i}')
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import trio
|
||||||
|
import tractor
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
debug_mode=True,
|
||||||
|
loglevel='cancel',
|
||||||
|
) as _root:
|
||||||
|
|
||||||
|
# manually trigger self-cancellation and wait
|
||||||
|
# for it to fully trigger.
|
||||||
|
_root.cancel_soon()
|
||||||
|
await _root._cancel_complete.wait()
|
||||||
|
print('root cancelled')
|
||||||
|
|
||||||
|
# now ensure we can still use the REPL
|
||||||
|
try:
|
||||||
|
await tractor.pause()
|
||||||
|
except trio.Cancelled as _taskc:
|
||||||
|
assert (root_cs := _root._root_tn.cancel_scope).cancel_called
|
||||||
|
# NOTE^^ above logic but inside `open_root_actor()` and
|
||||||
|
# passed to the `shield=` expression is effectively what
|
||||||
|
# we're testing here!
|
||||||
|
await tractor.pause(shield=root_cs.cancel_called)
|
||||||
|
|
||||||
|
# XXX, if shield logic *is wrong* inside `open_root_actor()`'s
|
||||||
|
# crash-handler block this should never be interacted,
|
||||||
|
# instead `trio.Cancelled` would be bubbled up: the original
|
||||||
|
# BUG.
|
||||||
|
assert 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
trio.run(main)
|
|
@ -23,9 +23,8 @@ async def main():
|
||||||
modules=[__name__]
|
modules=[__name__]
|
||||||
) as portal_map,
|
) as portal_map,
|
||||||
|
|
||||||
trio.open_nursery(
|
tractor.trionics.collapse_eg(),
|
||||||
strict_exception_groups=False,
|
trio.open_nursery() as tn,
|
||||||
) as tn,
|
|
||||||
):
|
):
|
||||||
|
|
||||||
for (name, portal) in portal_map.items():
|
for (name, portal) in portal_map.items():
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
from contextlib import (
|
||||||
|
contextmanager as cm,
|
||||||
|
# TODO, any diff in async case(s)??
|
||||||
|
# asynccontextmanager as acm,
|
||||||
|
)
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
import tractor
|
||||||
|
import trio
|
||||||
|
|
||||||
|
|
||||||
|
log = tractor.log.get_logger(__name__)
|
||||||
|
tractor.log.get_console_log('info')
|
||||||
|
|
||||||
|
@cm
|
||||||
|
def teardown_on_exc(
|
||||||
|
raise_from_handler: bool = False,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
You could also have a teardown handler which catches any exc and
|
||||||
|
does some required teardown. In this case the problem is
|
||||||
|
compounded UNLESS you ensure the handler's scope is OUTSIDE the
|
||||||
|
`ux.aclose()`.. that is in the caller's enclosing scope.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except BaseException as _berr:
|
||||||
|
berr = _berr
|
||||||
|
log.exception(
|
||||||
|
f'Handling termination teardown in child due to,\n'
|
||||||
|
f'{berr!r}\n'
|
||||||
|
)
|
||||||
|
if raise_from_handler:
|
||||||
|
# XXX teardown ops XXX
|
||||||
|
# on termination these steps say need to be run to
|
||||||
|
# ensure wider system consistency (like the state of
|
||||||
|
# remote connections/services).
|
||||||
|
#
|
||||||
|
# HOWEVER, any bug in this teardown code is also
|
||||||
|
# masked by the `tx.aclose()`!
|
||||||
|
# this is also true if `_tn.cancel_scope` is
|
||||||
|
# `.cancel_called` by the parent in a graceful
|
||||||
|
# request case..
|
||||||
|
|
||||||
|
# simulate a bug in teardown handler.
|
||||||
|
raise RuntimeError(
|
||||||
|
'woopsie teardown bug!'
|
||||||
|
)
|
||||||
|
|
||||||
|
raise # no teardown bug.
|
||||||
|
|
||||||
|
|
||||||
|
async def finite_stream_to_rent(
|
||||||
|
tx: trio.abc.SendChannel,
|
||||||
|
child_errors_mid_stream: bool,
|
||||||
|
|
||||||
|
task_status: trio.TaskStatus[
|
||||||
|
trio.CancelScope,
|
||||||
|
] = trio.TASK_STATUS_IGNORED,
|
||||||
|
):
|
||||||
|
async with (
|
||||||
|
# XXX without this unmasker the mid-streaming RTE is never
|
||||||
|
# reported since it is masked by the `tx.aclose()`
|
||||||
|
# call which in turn raises `Cancelled`!
|
||||||
|
#
|
||||||
|
# NOTE, this is WITHOUT doing any exception handling
|
||||||
|
# inside the child task!
|
||||||
|
#
|
||||||
|
# TODO, uncomment next LoC to see the supprsessed beg[RTE]!
|
||||||
|
# tractor.trionics.maybe_raise_from_masking_exc(),
|
||||||
|
|
||||||
|
tx as tx, # .aclose() is the guilty masker chkpt!
|
||||||
|
trio.open_nursery() as _tn,
|
||||||
|
):
|
||||||
|
# pass our scope back to parent for supervision\
|
||||||
|
# control.
|
||||||
|
task_status.started(_tn.cancel_scope)
|
||||||
|
|
||||||
|
with teardown_on_exc(
|
||||||
|
raise_from_handler=not child_errors_mid_stream,
|
||||||
|
):
|
||||||
|
for i in range(100):
|
||||||
|
log.info(
|
||||||
|
f'Child tx {i!r}\n'
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
child_errors_mid_stream
|
||||||
|
and
|
||||||
|
i == 66
|
||||||
|
):
|
||||||
|
# oh wait but WOOPS there's a bug
|
||||||
|
# in that teardown code!?
|
||||||
|
raise RuntimeError(
|
||||||
|
'woopsie, a mid-streaming bug!?'
|
||||||
|
)
|
||||||
|
|
||||||
|
await tx.send(i)
|
||||||
|
|
||||||
|
|
||||||
|
async def main(
|
||||||
|
# TODO! toggle this for the 2 cases!
|
||||||
|
# 1. child errors mid-stream while parent is also requesting
|
||||||
|
# (graceful) cancel of that child streamer.
|
||||||
|
#
|
||||||
|
# 2. child contains a teardown handler which contains a
|
||||||
|
# bug and raises.
|
||||||
|
#
|
||||||
|
child_errors_mid_stream: bool,
|
||||||
|
):
|
||||||
|
tx, rx = trio.open_memory_channel(1)
|
||||||
|
|
||||||
|
async with (
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
rx as rx,
|
||||||
|
):
|
||||||
|
|
||||||
|
_child_cs = await tn.start(
|
||||||
|
partial(
|
||||||
|
finite_stream_to_rent,
|
||||||
|
child_errors_mid_stream=child_errors_mid_stream,
|
||||||
|
tx=tx,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async for msg in rx:
|
||||||
|
log.info(
|
||||||
|
f'Rent rx {msg!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
# simulate some external cancellation
|
||||||
|
# request **JUST BEFORE** the child errors.
|
||||||
|
if msg == 65:
|
||||||
|
log.cancel(
|
||||||
|
f'Cancelling parent on,\n'
|
||||||
|
f'msg={msg}\n'
|
||||||
|
f'\n'
|
||||||
|
f'Simulates OOB cancel request!\n'
|
||||||
|
)
|
||||||
|
tn.cancel_scope.cancel()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
for case in [True, False]:
|
||||||
|
trio.run(main, case)
|
|
@ -1,13 +1,13 @@
|
||||||
"""
|
"""
|
||||||
That "native" debug mode better work!
|
That "native" debug mode better work!
|
||||||
|
|
||||||
All these tests can be understood (somewhat) by running the equivalent
|
All these tests can be understood (somewhat) by running the
|
||||||
`examples/debugging/` scripts manually.
|
equivalent `examples/debugging/` scripts manually.
|
||||||
|
|
||||||
TODO:
|
TODO:
|
||||||
- none of these tests have been run successfully on windows yet but
|
- none of these tests have been run successfully on windows yet but
|
||||||
there's been manual testing that verified it works.
|
there's been manual testing that verified it works.
|
||||||
- wonder if any of it'll work on OS X?
|
- wonder if any of it'll work on OS X?
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
@ -1175,6 +1175,54 @@ def test_ctxep_pauses_n_maybe_ipc_breaks(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_crash_handling_within_cancelled_root_actor(
|
||||||
|
spawn: PexpectSpawner,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Ensure that when only a root-actor is started via `open_root_actor()`
|
||||||
|
we can crash-handle in debug-mode despite self-cancellation.
|
||||||
|
|
||||||
|
More-or-less ensures we conditionally shield the pause in
|
||||||
|
`._root.open_root_actor()`'s `await debug._maybe_enter_pm()`
|
||||||
|
call.
|
||||||
|
|
||||||
|
'''
|
||||||
|
child = spawn('root_self_cancelled_w_error')
|
||||||
|
child.expect(PROMPT)
|
||||||
|
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
"Actor.cancel_soon()` was called!",
|
||||||
|
"root cancelled",
|
||||||
|
_pause_msg,
|
||||||
|
"('root'", # actor name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
child.expect(PROMPT)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
_crash_msg,
|
||||||
|
"('root'", # actor name
|
||||||
|
"AssertionError",
|
||||||
|
"assert 0",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
child.sendline('c')
|
||||||
|
child.expect(EOF)
|
||||||
|
assert_before(
|
||||||
|
child,
|
||||||
|
[
|
||||||
|
"AssertionError",
|
||||||
|
"assert 0",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: better error for "non-ideal" usage from the root actor.
|
# TODO: better error for "non-ideal" usage from the root actor.
|
||||||
# -[ ] if called from an async scope emit a message that suggests
|
# -[ ] if called from an async scope emit a message that suggests
|
||||||
# using `await tractor.pause()` instead since it's less overhead
|
# using `await tractor.pause()` instead since it's less overhead
|
||||||
|
|
|
@ -18,8 +18,9 @@ from tractor import (
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def bindspace_dir_str() -> str:
|
def bindspace_dir_str() -> str:
|
||||||
|
|
||||||
bs_dir_str: str = '/run/user/1000/doggy'
|
rt_dir: Path = tractor._state.get_rt_dir()
|
||||||
bs_dir = Path(bs_dir_str)
|
bs_dir: Path = rt_dir / 'doggy'
|
||||||
|
bs_dir_str: str = str(bs_dir)
|
||||||
assert not bs_dir.is_dir()
|
assert not bs_dir.is_dir()
|
||||||
|
|
||||||
yield bs_dir_str
|
yield bs_dir_str
|
||||||
|
|
|
@ -313,9 +313,8 @@ async def inf_streamer(
|
||||||
# `trio.EndOfChannel` doesn't propagate directly to the above
|
# `trio.EndOfChannel` doesn't propagate directly to the above
|
||||||
# .open_stream() parent, resulting in it also raising instead
|
# .open_stream() parent, resulting in it also raising instead
|
||||||
# of gracefully absorbing as normal.. so how to handle?
|
# of gracefully absorbing as normal.. so how to handle?
|
||||||
trio.open_nursery(
|
tractor.trionics.collapse_eg(),
|
||||||
strict_exception_groups=False,
|
trio.open_nursery() as tn,
|
||||||
) as tn,
|
|
||||||
):
|
):
|
||||||
async def close_stream_on_sentinel():
|
async def close_stream_on_sentinel():
|
||||||
async for msg in stream:
|
async for msg in stream:
|
||||||
|
|
|
@ -251,7 +251,10 @@ async def test_cancel_infinite_streamer(
|
||||||
start_method: str,
|
start_method: str,
|
||||||
):
|
):
|
||||||
# stream for at most 1 seconds
|
# stream for at most 1 seconds
|
||||||
with trio.move_on_after(1) as cancel_scope:
|
with (
|
||||||
|
trio.fail_after(4),
|
||||||
|
trio.move_on_after(1) as cancel_scope
|
||||||
|
):
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as n:
|
||||||
portal = await n.start_actor(
|
portal = await n.start_actor(
|
||||||
'donny',
|
'donny',
|
||||||
|
@ -561,10 +564,15 @@ def test_cancel_via_SIGINT_other_task(
|
||||||
async def main():
|
async def main():
|
||||||
# should never timeout since SIGINT should cancel the current program
|
# should never timeout since SIGINT should cancel the current program
|
||||||
with trio.fail_after(timeout):
|
with trio.fail_after(timeout):
|
||||||
async with trio.open_nursery(
|
async with (
|
||||||
strict_exception_groups=False,
|
|
||||||
) as n:
|
# XXX ?TODO? why no work!?
|
||||||
await n.start(spawn_and_sleep_forever)
|
# tractor.trionics.collapse_eg(),
|
||||||
|
trio.open_nursery(
|
||||||
|
strict_exception_groups=False,
|
||||||
|
) as tn,
|
||||||
|
):
|
||||||
|
await tn.start(spawn_and_sleep_forever)
|
||||||
if 'mp' in spawn_backend:
|
if 'mp' in spawn_backend:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
os.kill(pid, signal.SIGINT)
|
os.kill(pid, signal.SIGINT)
|
||||||
|
@ -575,40 +583,123 @@ def test_cancel_via_SIGINT_other_task(
|
||||||
|
|
||||||
async def spin_for(period=3):
|
async def spin_for(period=3):
|
||||||
"Sync sleep."
|
"Sync sleep."
|
||||||
|
print(f'sync sleeping in sub-sub for {period}\n')
|
||||||
time.sleep(period)
|
time.sleep(period)
|
||||||
|
|
||||||
|
|
||||||
async def spawn():
|
async def spawn_sub_with_sync_blocking_task():
|
||||||
async with tractor.open_nursery() as tn:
|
async with tractor.open_nursery() as an:
|
||||||
await tn.run_in_actor(
|
print('starting sync blocking subactor..\n')
|
||||||
|
await an.run_in_actor(
|
||||||
spin_for,
|
spin_for,
|
||||||
name='sleeper',
|
name='sleeper',
|
||||||
)
|
)
|
||||||
|
print('exiting first subactor layer..\n')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'man_cancel_outer',
|
||||||
|
[
|
||||||
|
False, # passes if delay != 2
|
||||||
|
|
||||||
|
# always causes an unexpected eg-w-embedded-assert-err?
|
||||||
|
pytest.param(True,
|
||||||
|
marks=pytest.mark.xfail(
|
||||||
|
reason=(
|
||||||
|
'always causes an unexpected eg-w-embedded-assert-err?'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
@no_windows
|
@no_windows
|
||||||
def test_cancel_while_childs_child_in_sync_sleep(
|
def test_cancel_while_childs_child_in_sync_sleep(
|
||||||
loglevel,
|
loglevel: str,
|
||||||
start_method,
|
start_method: str,
|
||||||
spawn_backend,
|
spawn_backend: str,
|
||||||
|
debug_mode: bool,
|
||||||
|
reg_addr: tuple,
|
||||||
|
man_cancel_outer: bool,
|
||||||
):
|
):
|
||||||
"""Verify that a child cancelled while executing sync code is torn
|
'''
|
||||||
|
Verify that a child cancelled while executing sync code is torn
|
||||||
down even when that cancellation is triggered by the parent
|
down even when that cancellation is triggered by the parent
|
||||||
2 nurseries "up".
|
2 nurseries "up".
|
||||||
"""
|
|
||||||
|
Though the grandchild should stay blocking its actor runtime, its
|
||||||
|
parent should issue a "zombie reaper" to hard kill it after
|
||||||
|
sufficient timeout.
|
||||||
|
|
||||||
|
'''
|
||||||
if start_method == 'forkserver':
|
if start_method == 'forkserver':
|
||||||
pytest.skip("Forksever sux hard at resuming from sync sleep...")
|
pytest.skip("Forksever sux hard at resuming from sync sleep...")
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
with trio.fail_after(2):
|
#
|
||||||
|
# XXX BIG TODO NOTE XXX
|
||||||
|
#
|
||||||
|
# it seems there's a strange race that can happen
|
||||||
|
# where where the fail-after will trigger outer scope
|
||||||
|
# .cancel() which then causes the inner scope to raise,
|
||||||
|
#
|
||||||
|
# BaseExceptionGroup('Exceptions from Trio nursery', [
|
||||||
|
# BaseExceptionGroup('Exceptions from Trio nursery',
|
||||||
|
# [
|
||||||
|
# Cancelled(),
|
||||||
|
# Cancelled(),
|
||||||
|
# ]
|
||||||
|
# ),
|
||||||
|
# AssertionError('assert 0')
|
||||||
|
# ])
|
||||||
|
#
|
||||||
|
# WHY THIS DOESN'T MAKE SENSE:
|
||||||
|
# ---------------------------
|
||||||
|
# - it should raise too-slow-error when too slow..
|
||||||
|
# * verified that using simple-cs and manually cancelling
|
||||||
|
# you get same outcome -> indicates that the fail-after
|
||||||
|
# can have its TooSlowError overriden!
|
||||||
|
# |_ to check this it's easy, simplly decrease the timeout
|
||||||
|
# as per the var below.
|
||||||
|
#
|
||||||
|
# - when using the manual simple-cs the outcome is different
|
||||||
|
# DESPITE the `assert 0` which means regardless of the
|
||||||
|
# inner scope effectively failing in the same way, the
|
||||||
|
# bubbling up **is NOT the same**.
|
||||||
|
#
|
||||||
|
# delays trigger diff outcomes..
|
||||||
|
# ---------------------------
|
||||||
|
# as seen by uncommenting various lines below there is from
|
||||||
|
# my POV an unexpected outcome due to the delay=2 case.
|
||||||
|
#
|
||||||
|
# delay = 1 # no AssertionError in eg, TooSlowError raised.
|
||||||
|
# delay = 2 # is AssertionError in eg AND no TooSlowError !?
|
||||||
|
delay = 4 # is AssertionError in eg AND no _cs cancellation.
|
||||||
|
|
||||||
|
with trio.fail_after(delay) as _cs:
|
||||||
|
# with trio.CancelScope() as cs:
|
||||||
|
# ^XXX^ can be used instead to see same outcome.
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
tractor.open_nursery() as an
|
# tractor.trionics.collapse_eg(), # doesn't help
|
||||||
|
tractor.open_nursery(
|
||||||
|
hide_tb=False,
|
||||||
|
debug_mode=debug_mode,
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
) as an,
|
||||||
):
|
):
|
||||||
await an.run_in_actor(
|
await an.run_in_actor(
|
||||||
spawn,
|
spawn_sub_with_sync_blocking_task,
|
||||||
name='spawn',
|
name='sync_blocking_sub',
|
||||||
)
|
)
|
||||||
await trio.sleep(1)
|
await trio.sleep(1)
|
||||||
|
|
||||||
|
if man_cancel_outer:
|
||||||
|
print('Cancelling manually in root')
|
||||||
|
_cs.cancel()
|
||||||
|
|
||||||
|
# trigger exc-srced taskc down
|
||||||
|
# the actor tree.
|
||||||
|
print('RAISING IN ROOT')
|
||||||
assert 0
|
assert 0
|
||||||
|
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
|
|
|
@ -117,9 +117,10 @@ async def open_actor_local_nursery(
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
):
|
):
|
||||||
global _nursery
|
global _nursery
|
||||||
async with trio.open_nursery(
|
async with (
|
||||||
strict_exception_groups=False,
|
tractor.trionics.collapse_eg(),
|
||||||
) as tn:
|
trio.open_nursery() as tn
|
||||||
|
):
|
||||||
_nursery = tn
|
_nursery = tn
|
||||||
await ctx.started()
|
await ctx.started()
|
||||||
await trio.sleep(10)
|
await trio.sleep(10)
|
||||||
|
|
|
@ -13,26 +13,24 @@ MESSAGE = 'tractoring at full speed'
|
||||||
def test_empty_mngrs_input_raises() -> None:
|
def test_empty_mngrs_input_raises() -> None:
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
with trio.fail_after(1):
|
with trio.fail_after(3):
|
||||||
async with (
|
async with (
|
||||||
open_actor_cluster(
|
open_actor_cluster(
|
||||||
modules=[__name__],
|
modules=[__name__],
|
||||||
|
|
||||||
# NOTE: ensure we can passthrough runtime opts
|
# NOTE: ensure we can passthrough runtime opts
|
||||||
loglevel='info',
|
loglevel='cancel',
|
||||||
# debug_mode=True,
|
debug_mode=False,
|
||||||
|
|
||||||
) as portals,
|
) as portals,
|
||||||
|
|
||||||
gather_contexts(
|
gather_contexts(mngrs=()),
|
||||||
# NOTE: it's the use of inline-generator syntax
|
|
||||||
# here that causes the empty input.
|
|
||||||
mngrs=(
|
|
||||||
p.open_context(worker) for p in portals.values()
|
|
||||||
),
|
|
||||||
),
|
|
||||||
):
|
):
|
||||||
assert 0
|
# should fail before this?
|
||||||
|
assert portals
|
||||||
|
|
||||||
|
# test should fail if we mk it here!
|
||||||
|
assert 0, 'Should have raised val-err !?'
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
@ -11,6 +11,7 @@ import psutil
|
||||||
import pytest
|
import pytest
|
||||||
import subprocess
|
import subprocess
|
||||||
import tractor
|
import tractor
|
||||||
|
from tractor.trionics import collapse_eg
|
||||||
from tractor._testing import tractor_test
|
from tractor._testing import tractor_test
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
@ -193,10 +194,10 @@ async def spawn_and_check_registry(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as an:
|
||||||
async with trio.open_nursery(
|
async with (
|
||||||
strict_exception_groups=False,
|
collapse_eg(),
|
||||||
) as trion:
|
trio.open_nursery() as trion,
|
||||||
|
):
|
||||||
portals = {}
|
portals = {}
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
name = f'a{i}'
|
name = f'a{i}'
|
||||||
|
@ -338,11 +339,12 @@ async def close_chans_before_nursery(
|
||||||
async with portal2.open_stream_from(
|
async with portal2.open_stream_from(
|
||||||
stream_forever
|
stream_forever
|
||||||
) as agen2:
|
) as agen2:
|
||||||
async with trio.open_nursery(
|
async with (
|
||||||
strict_exception_groups=False,
|
collapse_eg(),
|
||||||
) as n:
|
trio.open_nursery() as tn,
|
||||||
n.start_soon(streamer, agen1)
|
):
|
||||||
n.start_soon(cancel, use_signal, .5)
|
tn.start_soon(streamer, agen1)
|
||||||
|
tn.start_soon(cancel, use_signal, .5)
|
||||||
try:
|
try:
|
||||||
await streamer(agen2)
|
await streamer(agen2)
|
||||||
finally:
|
finally:
|
||||||
|
|
|
@ -234,10 +234,8 @@ async def trio_ctx(
|
||||||
with trio.fail_after(1 + delay):
|
with trio.fail_after(1 + delay):
|
||||||
try:
|
try:
|
||||||
async with (
|
async with (
|
||||||
trio.open_nursery(
|
tractor.trionics.collapse_eg(),
|
||||||
# TODO, for new `trio` / py3.13
|
trio.open_nursery() as tn,
|
||||||
# strict_exception_groups=False,
|
|
||||||
) as tn,
|
|
||||||
tractor.to_asyncio.open_channel_from(
|
tractor.to_asyncio.open_channel_from(
|
||||||
sleep_and_err,
|
sleep_and_err,
|
||||||
) as (first, chan),
|
) as (first, chan),
|
||||||
|
|
|
@ -235,10 +235,16 @@ async def cancel_after(wait, reg_addr):
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def time_quad_ex(reg_addr, ci_env, spawn_backend):
|
def time_quad_ex(
|
||||||
|
reg_addr: tuple,
|
||||||
|
ci_env: bool,
|
||||||
|
spawn_backend: str,
|
||||||
|
):
|
||||||
if spawn_backend == 'mp':
|
if spawn_backend == 'mp':
|
||||||
"""no idea but the mp *nix runs are flaking out here often...
|
'''
|
||||||
"""
|
no idea but the mp *nix runs are flaking out here often...
|
||||||
|
|
||||||
|
'''
|
||||||
pytest.skip("Test is too flaky on mp in CI")
|
pytest.skip("Test is too flaky on mp in CI")
|
||||||
|
|
||||||
timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4
|
timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4
|
||||||
|
@ -249,12 +255,24 @@ def time_quad_ex(reg_addr, ci_env, spawn_backend):
|
||||||
return results, diff
|
return results, diff
|
||||||
|
|
||||||
|
|
||||||
def test_a_quadruple_example(time_quad_ex, ci_env, spawn_backend):
|
def test_a_quadruple_example(
|
||||||
"""This also serves as a kind of "we'd like to be this fast test"."""
|
time_quad_ex: tuple,
|
||||||
|
ci_env: bool,
|
||||||
|
spawn_backend: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
This also serves as a kind of "we'd like to be this fast test".
|
||||||
|
|
||||||
|
'''
|
||||||
results, diff = time_quad_ex
|
results, diff = time_quad_ex
|
||||||
assert results
|
assert results
|
||||||
this_fast = 6 if platform.system() in ('Windows', 'Darwin') else 3
|
this_fast = (
|
||||||
|
6 if platform.system() in (
|
||||||
|
'Windows',
|
||||||
|
'Darwin',
|
||||||
|
)
|
||||||
|
else 3
|
||||||
|
)
|
||||||
assert diff < this_fast
|
assert diff < this_fast
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from contextlib import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from tractor.trionics import collapse_eg
|
||||||
import trio
|
import trio
|
||||||
from trio import TaskStatus
|
from trio import TaskStatus
|
||||||
|
|
||||||
|
@ -64,9 +65,8 @@ def test_stashed_child_nursery(use_start_soon):
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
trio.open_nursery(
|
collapse_eg(),
|
||||||
strict_exception_groups=False,
|
trio.open_nursery() as pn,
|
||||||
) as pn,
|
|
||||||
):
|
):
|
||||||
cn = await pn.start(mk_child_nursery)
|
cn = await pn.start(mk_child_nursery)
|
||||||
assert cn
|
assert cn
|
||||||
|
@ -195,10 +195,8 @@ def test_gatherctxs_with_memchan_breaks_multicancelled(
|
||||||
async with (
|
async with (
|
||||||
# XXX should ensure ONLY the KBI
|
# XXX should ensure ONLY the KBI
|
||||||
# is relayed upward
|
# is relayed upward
|
||||||
trionics.collapse_eg(),
|
collapse_eg(),
|
||||||
trio.open_nursery(
|
trio.open_nursery(), # as tn,
|
||||||
# strict_exception_groups=False,
|
|
||||||
), # as tn,
|
|
||||||
|
|
||||||
trionics.gather_contexts([
|
trionics.gather_contexts([
|
||||||
open_memchan(),
|
open_memchan(),
|
||||||
|
|
|
@ -55,10 +55,17 @@ async def open_actor_cluster(
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
'Number of names is {len(names)} but count it {count}')
|
'Number of names is {len(names)} but count it {count}')
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with (
|
||||||
**runtime_kwargs,
|
# tractor.trionics.collapse_eg(),
|
||||||
) as an:
|
tractor.open_nursery(
|
||||||
async with trio.open_nursery() as n:
|
**runtime_kwargs,
|
||||||
|
) as an
|
||||||
|
):
|
||||||
|
async with (
|
||||||
|
# tractor.trionics.collapse_eg(),
|
||||||
|
trio.open_nursery() as tn,
|
||||||
|
tractor.trionics.maybe_raise_from_masking_exc()
|
||||||
|
):
|
||||||
uid = tractor.current_actor().uid
|
uid = tractor.current_actor().uid
|
||||||
|
|
||||||
async def _start(name: str) -> None:
|
async def _start(name: str) -> None:
|
||||||
|
@ -69,9 +76,8 @@ async def open_actor_cluster(
|
||||||
)
|
)
|
||||||
|
|
||||||
for name in names:
|
for name in names:
|
||||||
n.start_soon(_start, name)
|
tn.start_soon(_start, name)
|
||||||
|
|
||||||
assert len(portals) == count
|
assert len(portals) == count
|
||||||
yield portals
|
yield portals
|
||||||
|
|
||||||
await an.cancel(hard_kill=hard_kill)
|
await an.cancel(hard_kill=hard_kill)
|
||||||
|
|
|
@ -88,8 +88,7 @@ async def maybe_block_bp(
|
||||||
bp_blocked: bool
|
bp_blocked: bool
|
||||||
if (
|
if (
|
||||||
debug_mode
|
debug_mode
|
||||||
and
|
and maybe_enable_greenback
|
||||||
maybe_enable_greenback
|
|
||||||
and (
|
and (
|
||||||
maybe_mod := await debug.maybe_init_greenback(
|
maybe_mod := await debug.maybe_init_greenback(
|
||||||
raise_not_found=False,
|
raise_not_found=False,
|
||||||
|
@ -479,16 +478,14 @@ async def open_root_actor(
|
||||||
|
|
||||||
# start runtime in a bg sub-task, yield to caller.
|
# start runtime in a bg sub-task, yield to caller.
|
||||||
async with (
|
async with (
|
||||||
collapse_eg(
|
collapse_eg(),
|
||||||
hide_tb=hide_tb,
|
|
||||||
# bp=True,
|
|
||||||
),
|
|
||||||
trio.open_nursery() as root_tn,
|
trio.open_nursery() as root_tn,
|
||||||
|
|
||||||
# XXX, finally-footgun below?
|
# ?TODO? finally-footgun below?
|
||||||
# -> see note on why shielding.
|
# -> see note on why shielding.
|
||||||
# maybe_raise_from_masking_exc(),
|
# maybe_raise_from_masking_exc(),
|
||||||
):
|
):
|
||||||
|
actor._root_tn = root_tn
|
||||||
# `_runtime.async_main()` creates an internal nursery
|
# `_runtime.async_main()` creates an internal nursery
|
||||||
# and blocks here until any underlying actor(-process)
|
# and blocks here until any underlying actor(-process)
|
||||||
# tree has terminated thereby conducting so called
|
# tree has terminated thereby conducting so called
|
||||||
|
@ -527,6 +524,11 @@ async def open_root_actor(
|
||||||
err,
|
err,
|
||||||
api_frame=inspect.currentframe(),
|
api_frame=inspect.currentframe(),
|
||||||
debug_filter=debug_filter,
|
debug_filter=debug_filter,
|
||||||
|
|
||||||
|
# XXX NOTE, required to debug root-actor
|
||||||
|
# crashes under cancellation conditions; so
|
||||||
|
# most of them!
|
||||||
|
shield=root_tn.cancel_scope.cancel_called,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -566,6 +568,7 @@ async def open_root_actor(
|
||||||
f'{op_nested_actor_repr}'
|
f'{op_nested_actor_repr}'
|
||||||
)
|
)
|
||||||
# XXX, THIS IS A *finally-footgun*!
|
# XXX, THIS IS A *finally-footgun*!
|
||||||
|
# (also mentioned in with-block above)
|
||||||
# -> though already shields iternally it can
|
# -> though already shields iternally it can
|
||||||
# taskc here and mask underlying errors raised in
|
# taskc here and mask underlying errors raised in
|
||||||
# the try-block above?
|
# the try-block above?
|
||||||
|
|
|
@ -284,10 +284,6 @@ async def _errors_relayed_via_ipc(
|
||||||
try:
|
try:
|
||||||
yield # run RPC invoke body
|
yield # run RPC invoke body
|
||||||
|
|
||||||
except TransportClosed:
|
|
||||||
log.exception('Tpt disconnect during remote-exc relay?')
|
|
||||||
raise
|
|
||||||
|
|
||||||
# box and ship RPC errors for wire-transit via
|
# box and ship RPC errors for wire-transit via
|
||||||
# the task's requesting parent IPC-channel.
|
# the task's requesting parent IPC-channel.
|
||||||
except (
|
except (
|
||||||
|
@ -323,9 +319,6 @@ async def _errors_relayed_via_ipc(
|
||||||
and debug_kbis
|
and debug_kbis
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# TODO? better then `debug_filter` below?
|
|
||||||
# and
|
|
||||||
# not isinstance(err, TransportClosed)
|
|
||||||
):
|
):
|
||||||
# XXX QUESTION XXX: is there any case where we'll
|
# XXX QUESTION XXX: is there any case where we'll
|
||||||
# want to debug IPC disconnects as a default?
|
# want to debug IPC disconnects as a default?
|
||||||
|
@ -334,25 +327,13 @@ async def _errors_relayed_via_ipc(
|
||||||
# recovery logic - the only case is some kind of
|
# recovery logic - the only case is some kind of
|
||||||
# strange bug in our transport layer itself? Going
|
# strange bug in our transport layer itself? Going
|
||||||
# to keep this open ended for now.
|
# to keep this open ended for now.
|
||||||
|
log.debug(
|
||||||
if _state.debug_mode():
|
'RPC task crashed, attempting to enter debugger\n'
|
||||||
log.exception(
|
f'|_{ctx}'
|
||||||
f'RPC task crashed!\n'
|
)
|
||||||
f'Attempting to enter debugger\n'
|
|
||||||
f'\n'
|
|
||||||
f'{ctx}'
|
|
||||||
)
|
|
||||||
|
|
||||||
entered_debug = await debug._maybe_enter_pm(
|
entered_debug = await debug._maybe_enter_pm(
|
||||||
err,
|
err,
|
||||||
api_frame=inspect.currentframe(),
|
api_frame=inspect.currentframe(),
|
||||||
|
|
||||||
# don't REPL any psuedo-expected tpt-disconnect
|
|
||||||
# debug_filter=lambda exc: (
|
|
||||||
# type (exc) not in {
|
|
||||||
# TransportClosed,
|
|
||||||
# }
|
|
||||||
# ),
|
|
||||||
)
|
)
|
||||||
if not entered_debug:
|
if not entered_debug:
|
||||||
# if we prolly should have entered the REPL but
|
# if we prolly should have entered the REPL but
|
||||||
|
@ -403,7 +384,7 @@ async def _errors_relayed_via_ipc(
|
||||||
|
|
||||||
# RPC task bookeeping.
|
# RPC task bookeeping.
|
||||||
# since RPC tasks are scheduled inside a flat
|
# since RPC tasks are scheduled inside a flat
|
||||||
# `Actor._service_n`, we add "handles" to each such that
|
# `Actor._service_tn`, we add "handles" to each such that
|
||||||
# they can be individually ccancelled.
|
# they can be individually ccancelled.
|
||||||
finally:
|
finally:
|
||||||
|
|
||||||
|
@ -469,7 +450,7 @@ async def _invoke(
|
||||||
kwargs: dict[str, Any],
|
kwargs: dict[str, Any],
|
||||||
|
|
||||||
is_rpc: bool = True,
|
is_rpc: bool = True,
|
||||||
hide_tb: bool = False,
|
hide_tb: bool = True,
|
||||||
return_msg_type: Return|CancelAck = Return,
|
return_msg_type: Return|CancelAck = Return,
|
||||||
|
|
||||||
task_status: TaskStatus[
|
task_status: TaskStatus[
|
||||||
|
@ -481,7 +462,7 @@ async def _invoke(
|
||||||
connected IPC channel.
|
connected IPC channel.
|
||||||
|
|
||||||
This is the core "RPC" `trio.Task` scheduling machinery used to start every
|
This is the core "RPC" `trio.Task` scheduling machinery used to start every
|
||||||
remotely invoked function, normally in `Actor._service_n: Nursery`.
|
remotely invoked function, normally in `Actor._service_tn: Nursery`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
@ -693,20 +674,7 @@ async def _invoke(
|
||||||
f'\n'
|
f'\n'
|
||||||
f'{pretty_struct.pformat(return_msg)}\n'
|
f'{pretty_struct.pformat(return_msg)}\n'
|
||||||
)
|
)
|
||||||
try:
|
await chan.send(return_msg)
|
||||||
await chan.send(return_msg)
|
|
||||||
except TransportClosed:
|
|
||||||
log.exception(
|
|
||||||
f"Failed send final result to 'parent'-side of IPC-ctx!\n"
|
|
||||||
f'\n'
|
|
||||||
f'{chan}\n'
|
|
||||||
f'Channel already disconnected ??\n'
|
|
||||||
f'\n'
|
|
||||||
f'{pretty_struct.pformat(return_msg)}'
|
|
||||||
)
|
|
||||||
# ?TODO? will this ever be true though?
|
|
||||||
if chan.connected():
|
|
||||||
raise
|
|
||||||
|
|
||||||
# NOTE: this happens IFF `ctx._scope.cancel()` is
|
# NOTE: this happens IFF `ctx._scope.cancel()` is
|
||||||
# called by any of,
|
# called by any of,
|
||||||
|
@ -967,7 +935,7 @@ async def process_messages(
|
||||||
|
|
||||||
Receive (multiplexed) per-`Channel` RPC requests as msgs from
|
Receive (multiplexed) per-`Channel` RPC requests as msgs from
|
||||||
remote processes; schedule target async funcs as local
|
remote processes; schedule target async funcs as local
|
||||||
`trio.Task`s inside the `Actor._service_n: Nursery`.
|
`trio.Task`s inside the `Actor._service_tn: Nursery`.
|
||||||
|
|
||||||
Depending on msg type, non-`cmd` (task spawning/starting)
|
Depending on msg type, non-`cmd` (task spawning/starting)
|
||||||
request payloads (eg. `started`, `yield`, `return`, `error`)
|
request payloads (eg. `started`, `yield`, `return`, `error`)
|
||||||
|
@ -992,7 +960,7 @@ async def process_messages(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
actor: Actor = _state.current_actor()
|
actor: Actor = _state.current_actor()
|
||||||
assert actor._service_n # runtime state sanity
|
assert actor._service_tn # runtime state sanity
|
||||||
|
|
||||||
# TODO: once `trio` get's an "obvious way" for req/resp we
|
# TODO: once `trio` get's an "obvious way" for req/resp we
|
||||||
# should use it?
|
# should use it?
|
||||||
|
@ -1203,7 +1171,7 @@ async def process_messages(
|
||||||
start_status += '->( scheduling new task..\n'
|
start_status += '->( scheduling new task..\n'
|
||||||
log.runtime(start_status)
|
log.runtime(start_status)
|
||||||
try:
|
try:
|
||||||
ctx: Context = await actor._service_n.start(
|
ctx: Context = await actor._service_tn.start(
|
||||||
partial(
|
partial(
|
||||||
_invoke,
|
_invoke,
|
||||||
actor,
|
actor,
|
||||||
|
@ -1343,7 +1311,7 @@ async def process_messages(
|
||||||
) as err:
|
) as err:
|
||||||
|
|
||||||
if nursery_cancelled_before_task:
|
if nursery_cancelled_before_task:
|
||||||
sn: Nursery = actor._service_n
|
sn: Nursery = actor._service_tn
|
||||||
assert sn and sn.cancel_scope.cancel_called # sanity
|
assert sn and sn.cancel_scope.cancel_called # sanity
|
||||||
log.cancel(
|
log.cancel(
|
||||||
f'Service nursery cancelled before it handled {funcname}'
|
f'Service nursery cancelled before it handled {funcname}'
|
||||||
|
|
|
@ -35,6 +35,15 @@ for running all lower level spawning, supervision and msging layers:
|
||||||
SC-transitive RPC via scheduling of `trio` tasks.
|
SC-transitive RPC via scheduling of `trio` tasks.
|
||||||
- registration of newly spawned actors with the discovery sys.
|
- registration of newly spawned actors with the discovery sys.
|
||||||
|
|
||||||
|
Glossary:
|
||||||
|
--------
|
||||||
|
- tn: a `trio.Nursery` or "task nursery".
|
||||||
|
- an: an `ActorNursery` or "actor nursery".
|
||||||
|
- root: top/parent-most scope/task/process/actor (or other runtime
|
||||||
|
primitive) in a hierarchical tree.
|
||||||
|
- parent-ish: "higher-up" in the runtime-primitive hierarchy.
|
||||||
|
- child-ish: "lower-down" in the runtime-primitive hierarchy.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from contextlib import (
|
from contextlib import (
|
||||||
|
@ -76,6 +85,7 @@ from tractor.msg import (
|
||||||
)
|
)
|
||||||
from .trionics import (
|
from .trionics import (
|
||||||
collapse_eg,
|
collapse_eg,
|
||||||
|
maybe_open_nursery,
|
||||||
)
|
)
|
||||||
from .ipc import (
|
from .ipc import (
|
||||||
Channel,
|
Channel,
|
||||||
|
@ -173,10 +183,11 @@ class Actor:
|
||||||
|
|
||||||
msg_buffer_size: int = 2**6
|
msg_buffer_size: int = 2**6
|
||||||
|
|
||||||
# nursery placeholders filled in by `async_main()` after fork
|
# nursery placeholders filled in by `async_main()`,
|
||||||
_root_n: Nursery|None = None
|
# - after fork for subactors.
|
||||||
_service_n: Nursery|None = None
|
# - during boot for the root actor.
|
||||||
|
_root_tn: Nursery|None = None
|
||||||
|
_service_tn: Nursery|None = None
|
||||||
_ipc_server: _server.IPCServer|None = None
|
_ipc_server: _server.IPCServer|None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1010,12 +1021,48 @@ class Actor:
|
||||||
the RPC service nursery.
|
the RPC service nursery.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
assert self._service_n
|
actor_repr: str = _pformat.nest_from_op(
|
||||||
self._service_n.start_soon(
|
input_op='>c(',
|
||||||
|
text=self.pformat(),
|
||||||
|
nest_indent=1,
|
||||||
|
)
|
||||||
|
log.cancel(
|
||||||
|
'Actor.cancel_soon()` was called!\n'
|
||||||
|
f'>> scheduling `Actor.cancel()`\n'
|
||||||
|
f'{actor_repr}'
|
||||||
|
)
|
||||||
|
assert self._service_tn
|
||||||
|
self._service_tn.start_soon(
|
||||||
self.cancel,
|
self.cancel,
|
||||||
None, # self cancel all rpc tasks
|
None, # self cancel all rpc tasks
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# schedule a "canceller task" in the `._root_tn` once the
|
||||||
|
# `._service_tn` is fully shutdown; task waits for child-ish
|
||||||
|
# scopes to fully exit then finally cancels its parent,
|
||||||
|
# root-most, scope.
|
||||||
|
async def cancel_root_tn_after_services():
|
||||||
|
log.runtime(
|
||||||
|
'Waiting on service-tn to cancel..\n'
|
||||||
|
f'c>)\n'
|
||||||
|
f'|_{self._service_tn.cancel_scope!r}\n'
|
||||||
|
)
|
||||||
|
await self._cancel_complete.wait()
|
||||||
|
log.cancel(
|
||||||
|
f'`._service_tn` cancelled\n'
|
||||||
|
f'>c)\n'
|
||||||
|
f'|_{self._service_tn.cancel_scope!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'>> cancelling `._root_tn`\n'
|
||||||
|
f'c>(\n'
|
||||||
|
f' |_{self._root_tn.cancel_scope!r}\n'
|
||||||
|
)
|
||||||
|
self._root_tn.cancel_scope.cancel()
|
||||||
|
|
||||||
|
self._root_tn.start_soon(
|
||||||
|
cancel_root_tn_after_services
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cancel_complete(self) -> bool:
|
def cancel_complete(self) -> bool:
|
||||||
return self._cancel_complete.is_set()
|
return self._cancel_complete.is_set()
|
||||||
|
@ -1120,8 +1167,8 @@ class Actor:
|
||||||
await ipc_server.wait_for_shutdown()
|
await ipc_server.wait_for_shutdown()
|
||||||
|
|
||||||
# cancel all rpc tasks permanently
|
# cancel all rpc tasks permanently
|
||||||
if self._service_n:
|
if self._service_tn:
|
||||||
self._service_n.cancel_scope.cancel()
|
self._service_tn.cancel_scope.cancel()
|
||||||
|
|
||||||
log_meth(msg)
|
log_meth(msg)
|
||||||
self._cancel_complete.set()
|
self._cancel_complete.set()
|
||||||
|
@ -1258,7 +1305,7 @@ class Actor:
|
||||||
'''
|
'''
|
||||||
Cancel all ongoing RPC tasks owned/spawned for a given
|
Cancel all ongoing RPC tasks owned/spawned for a given
|
||||||
`parent_chan: Channel` or simply all tasks (inside
|
`parent_chan: Channel` or simply all tasks (inside
|
||||||
`._service_n`) when `parent_chan=None`.
|
`._service_tn`) when `parent_chan=None`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
tasks: dict = self._rpc_tasks
|
tasks: dict = self._rpc_tasks
|
||||||
|
@ -1470,46 +1517,55 @@ async def async_main(
|
||||||
accept_addrs.append(addr.unwrap())
|
accept_addrs.append(addr.unwrap())
|
||||||
|
|
||||||
assert accept_addrs
|
assert accept_addrs
|
||||||
# The "root" nursery ensures the channel with the immediate
|
|
||||||
# parent is kept alive as a resilient service until
|
ya_root_tn: bool = bool(actor._root_tn)
|
||||||
# cancellation steps have (mostly) occurred in
|
ya_service_tn: bool = bool(actor._service_tn)
|
||||||
# a deterministic way.
|
|
||||||
|
# NOTE, a top-most "root" nursery in each actor-process
|
||||||
|
# enables a lifetime priority for the IPC-channel connection
|
||||||
|
# with a sub-actor's immediate parent. I.e. this connection
|
||||||
|
# is kept alive as a resilient service connection until all
|
||||||
|
# other machinery has exited, cancellation of all
|
||||||
|
# embedded/child scopes have completed. This helps ensure
|
||||||
|
# a deterministic (and thus "graceful")
|
||||||
|
# first-class-supervision style teardown where a parent actor
|
||||||
|
# (vs. say peers) is always the last to be contacted before
|
||||||
|
# disconnect.
|
||||||
root_tn: trio.Nursery
|
root_tn: trio.Nursery
|
||||||
async with (
|
async with (
|
||||||
collapse_eg(),
|
collapse_eg(),
|
||||||
trio.open_nursery() as root_tn,
|
maybe_open_nursery(
|
||||||
|
nursery=actor._root_tn,
|
||||||
|
) as root_tn,
|
||||||
):
|
):
|
||||||
actor._root_n = root_tn
|
if ya_root_tn:
|
||||||
assert actor._root_n
|
assert root_tn is actor._root_tn
|
||||||
|
else:
|
||||||
|
actor._root_tn = root_tn
|
||||||
|
|
||||||
ipc_server: _server.IPCServer
|
ipc_server: _server.IPCServer
|
||||||
async with (
|
async with (
|
||||||
collapse_eg(),
|
collapse_eg(),
|
||||||
trio.open_nursery() as service_nursery,
|
maybe_open_nursery(
|
||||||
|
nursery=actor._service_tn,
|
||||||
|
) as service_tn,
|
||||||
_server.open_ipc_server(
|
_server.open_ipc_server(
|
||||||
parent_tn=service_nursery,
|
parent_tn=service_tn, # ?TODO, why can't this be the root-tn
|
||||||
stream_handler_tn=service_nursery,
|
stream_handler_tn=service_tn,
|
||||||
) as ipc_server,
|
) as ipc_server,
|
||||||
# ) as actor._ipc_server,
|
|
||||||
# ^TODO? prettier?
|
|
||||||
|
|
||||||
):
|
):
|
||||||
# This nursery is used to handle all inbound
|
if ya_service_tn:
|
||||||
# connections to us such that if the TCP server
|
assert service_tn is actor._service_tn
|
||||||
# is killed, connections can continue to process
|
else:
|
||||||
# in the background until this nursery is cancelled.
|
# This nursery is used to handle all inbound
|
||||||
actor._service_n = service_nursery
|
# connections to us such that if the TCP server
|
||||||
|
# is killed, connections can continue to process
|
||||||
|
# in the background until this nursery is cancelled.
|
||||||
|
actor._service_tn = service_tn
|
||||||
|
|
||||||
|
# set after allocate
|
||||||
actor._ipc_server = ipc_server
|
actor._ipc_server = ipc_server
|
||||||
assert (
|
|
||||||
actor._service_n
|
|
||||||
and (
|
|
||||||
actor._service_n
|
|
||||||
is
|
|
||||||
actor._ipc_server._parent_tn
|
|
||||||
is
|
|
||||||
ipc_server._stream_handler_tn
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# load exposed/allowed RPC modules
|
# load exposed/allowed RPC modules
|
||||||
# XXX: do this **after** establishing a channel to the parent
|
# XXX: do this **after** establishing a channel to the parent
|
||||||
|
@ -1535,10 +1591,11 @@ async def async_main(
|
||||||
# - root actor: the ``accept_addr`` passed to this method
|
# - root actor: the ``accept_addr`` passed to this method
|
||||||
|
|
||||||
# TODO: why is this not with the root nursery?
|
# TODO: why is this not with the root nursery?
|
||||||
|
# - see above that the `._service_tn` is what's used?
|
||||||
try:
|
try:
|
||||||
eps: list = await ipc_server.listen_on(
|
eps: list = await ipc_server.listen_on(
|
||||||
accept_addrs=accept_addrs,
|
accept_addrs=accept_addrs,
|
||||||
stream_handler_nursery=service_nursery,
|
stream_handler_nursery=service_tn,
|
||||||
)
|
)
|
||||||
log.runtime(
|
log.runtime(
|
||||||
f'Booted IPC server\n'
|
f'Booted IPC server\n'
|
||||||
|
@ -1546,7 +1603,7 @@ async def async_main(
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
(eps[0].listen_tn)
|
(eps[0].listen_tn)
|
||||||
is not service_nursery
|
is not service_tn
|
||||||
)
|
)
|
||||||
|
|
||||||
except OSError as oserr:
|
except OSError as oserr:
|
||||||
|
@ -1708,7 +1765,7 @@ async def async_main(
|
||||||
|
|
||||||
# XXX TODO but hard XXX
|
# XXX TODO but hard XXX
|
||||||
# we can't actually do this bc the debugger uses the
|
# we can't actually do this bc the debugger uses the
|
||||||
# _service_n to spawn the lock task, BUT, in theory if we had
|
# _service_tn to spawn the lock task, BUT, in theory if we had
|
||||||
# the root nursery surround this finally block it might be
|
# the root nursery surround this finally block it might be
|
||||||
# actually possible to debug THIS machinery in the same way
|
# actually possible to debug THIS machinery in the same way
|
||||||
# as user task code?
|
# as user task code?
|
||||||
|
|
|
@ -335,6 +335,23 @@ async def hard_kill(
|
||||||
# zombies (as a feature) we ask the OS to do send in the
|
# zombies (as a feature) we ask the OS to do send in the
|
||||||
# removal swad as the last resort.
|
# removal swad as the last resort.
|
||||||
if cs.cancelled_caught:
|
if cs.cancelled_caught:
|
||||||
|
|
||||||
|
# TODO? attempt at intermediary-rent-sub
|
||||||
|
# with child in debug lock?
|
||||||
|
# |_https://github.com/goodboy/tractor/issues/320
|
||||||
|
#
|
||||||
|
# if not is_root_process():
|
||||||
|
# log.warning(
|
||||||
|
# 'Attempting to acquire debug-REPL-lock before zombie reap!'
|
||||||
|
# )
|
||||||
|
# with trio.CancelScope(shield=True):
|
||||||
|
# async with debug.acquire_debug_lock(
|
||||||
|
# subactor_uid=current_actor().uid,
|
||||||
|
# ) as _ctx:
|
||||||
|
# log.warning(
|
||||||
|
# 'Acquired debug lock, child ready to be killed ??\n'
|
||||||
|
# )
|
||||||
|
|
||||||
# TODO: toss in the skynet-logo face as ascii art?
|
# TODO: toss in the skynet-logo face as ascii art?
|
||||||
log.critical(
|
log.critical(
|
||||||
# 'Well, the #ZOMBIE_LORD_IS_HERE# to collect\n'
|
# 'Well, the #ZOMBIE_LORD_IS_HERE# to collect\n'
|
||||||
|
|
|
@ -749,8 +749,9 @@ _shutdown_msg: str = (
|
||||||
'Actor-runtime-shutdown'
|
'Actor-runtime-shutdown'
|
||||||
)
|
)
|
||||||
|
|
||||||
# @api_frame
|
|
||||||
@acm
|
@acm
|
||||||
|
# @api_frame
|
||||||
async def open_nursery(
|
async def open_nursery(
|
||||||
*, # named params only!
|
*, # named params only!
|
||||||
hide_tb: bool = False,
|
hide_tb: bool = False,
|
||||||
|
|
|
@ -481,12 +481,12 @@ async def _pause(
|
||||||
# we have to figure out how to avoid having the service nursery
|
# we have to figure out how to avoid having the service nursery
|
||||||
# cancel on this task start? I *think* this works below:
|
# cancel on this task start? I *think* this works below:
|
||||||
# ```python
|
# ```python
|
||||||
# actor._service_n.cancel_scope.shield = shield
|
# actor._service_tn.cancel_scope.shield = shield
|
||||||
# ```
|
# ```
|
||||||
# but not entirely sure if that's a sane way to implement it?
|
# but not entirely sure if that's a sane way to implement it?
|
||||||
|
|
||||||
# NOTE currently we spawn the lock request task inside this
|
# NOTE currently we spawn the lock request task inside this
|
||||||
# subactor's global `Actor._service_n` so that the
|
# subactor's global `Actor._service_tn` so that the
|
||||||
# lifetime of the lock-request can outlive the current
|
# lifetime of the lock-request can outlive the current
|
||||||
# `._pause()` scope while the user steps through their
|
# `._pause()` scope while the user steps through their
|
||||||
# application code and when they finally exit the
|
# application code and when they finally exit the
|
||||||
|
@ -510,7 +510,7 @@ async def _pause(
|
||||||
f'|_{task}\n'
|
f'|_{task}\n'
|
||||||
)
|
)
|
||||||
with trio.CancelScope(shield=shield):
|
with trio.CancelScope(shield=shield):
|
||||||
req_ctx: Context = await actor._service_n.start(
|
req_ctx: Context = await actor._service_tn.start(
|
||||||
partial(
|
partial(
|
||||||
request_root_stdio_lock,
|
request_root_stdio_lock,
|
||||||
actor_uid=actor.uid,
|
actor_uid=actor.uid,
|
||||||
|
@ -544,7 +544,7 @@ async def _pause(
|
||||||
_repl_fail_report = None
|
_repl_fail_report = None
|
||||||
|
|
||||||
# when the actor is mid-runtime cancellation the
|
# when the actor is mid-runtime cancellation the
|
||||||
# `Actor._service_n` might get closed before we can spawn
|
# `Actor._service_tn` might get closed before we can spawn
|
||||||
# the request task, so just ignore expected RTE.
|
# the request task, so just ignore expected RTE.
|
||||||
elif (
|
elif (
|
||||||
isinstance(pause_err, RuntimeError)
|
isinstance(pause_err, RuntimeError)
|
||||||
|
@ -989,7 +989,7 @@ def pause_from_sync(
|
||||||
# that output and assign the `repl` created above!
|
# that output and assign the `repl` created above!
|
||||||
bg_task, _ = trio.from_thread.run(
|
bg_task, _ = trio.from_thread.run(
|
||||||
afn=partial(
|
afn=partial(
|
||||||
actor._service_n.start,
|
actor._service_tn.start,
|
||||||
partial(
|
partial(
|
||||||
_pause_from_bg_root_thread,
|
_pause_from_bg_root_thread,
|
||||||
behalf_of_thread=thread,
|
behalf_of_thread=thread,
|
||||||
|
|
|
@ -17,36 +17,59 @@
|
||||||
Utils to tame mp non-SC madeness
|
Utils to tame mp non-SC madeness
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
import platform
|
||||||
|
|
||||||
|
|
||||||
# !TODO! in 3.13 this can be disabled (the-same/similarly) using
|
|
||||||
# a flag,
|
|
||||||
# - [ ] soo if it works like this, drop this module entirely for
|
|
||||||
# 3.13+ B)
|
|
||||||
# |_https://docs.python.org/3/library/multiprocessing.shared_memory.html
|
|
||||||
#
|
|
||||||
def disable_mantracker():
|
def disable_mantracker():
|
||||||
'''
|
'''
|
||||||
Disable all `multiprocessing` "resource tracking" machinery since
|
Disable all `multiprocessing` "resource tracking" machinery since
|
||||||
it's an absolute multi-threaded mess of non-SC madness.
|
it's an absolute multi-threaded mess of non-SC madness.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from multiprocessing import resource_tracker as mantracker
|
from multiprocessing.shared_memory import SharedMemory
|
||||||
|
|
||||||
# Tell the "resource tracker" thing to fuck off.
|
|
||||||
class ManTracker(mantracker.ResourceTracker):
|
|
||||||
def register(self, name, rtype):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def unregister(self, name, rtype):
|
# 3.13+ only.. can pass `track=False` to disable
|
||||||
pass
|
# all the resource tracker bs.
|
||||||
|
# https://docs.python.org/3/library/multiprocessing.shared_memory.html
|
||||||
|
if (_py_313 := (
|
||||||
|
platform.python_version_tuple()[:-1]
|
||||||
|
>=
|
||||||
|
('3', '13')
|
||||||
|
)
|
||||||
|
):
|
||||||
|
from functools import partial
|
||||||
|
return partial(
|
||||||
|
SharedMemory,
|
||||||
|
track=False,
|
||||||
|
)
|
||||||
|
|
||||||
def ensure_running(self):
|
# !TODO, once we drop 3.12- we can obvi remove all this!
|
||||||
pass
|
else:
|
||||||
|
from multiprocessing import (
|
||||||
|
resource_tracker as mantracker,
|
||||||
|
)
|
||||||
|
|
||||||
# "know your land and know your prey"
|
# Tell the "resource tracker" thing to fuck off.
|
||||||
# https://www.dailymotion.com/video/x6ozzco
|
class ManTracker(mantracker.ResourceTracker):
|
||||||
mantracker._resource_tracker = ManTracker()
|
def register(self, name, rtype):
|
||||||
mantracker.register = mantracker._resource_tracker.register
|
pass
|
||||||
mantracker.ensure_running = mantracker._resource_tracker.ensure_running
|
|
||||||
mantracker.unregister = mantracker._resource_tracker.unregister
|
def unregister(self, name, rtype):
|
||||||
mantracker.getfd = mantracker._resource_tracker.getfd
|
pass
|
||||||
|
|
||||||
|
def ensure_running(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# "know your land and know your prey"
|
||||||
|
# https://www.dailymotion.com/video/x6ozzco
|
||||||
|
mantracker._resource_tracker = ManTracker()
|
||||||
|
mantracker.register = mantracker._resource_tracker.register
|
||||||
|
mantracker.ensure_running = mantracker._resource_tracker.ensure_running
|
||||||
|
mantracker.unregister = mantracker._resource_tracker.unregister
|
||||||
|
mantracker.getfd = mantracker._resource_tracker.getfd
|
||||||
|
|
||||||
|
# use std type verbatim
|
||||||
|
shmT = SharedMemory
|
||||||
|
|
||||||
|
return shmT
|
||||||
|
|
|
@ -1001,7 +1001,11 @@ class Server(Struct):
|
||||||
partial(
|
partial(
|
||||||
_serve_ipc_eps,
|
_serve_ipc_eps,
|
||||||
server=self,
|
server=self,
|
||||||
stream_handler_tn=stream_handler_nursery,
|
stream_handler_tn=(
|
||||||
|
stream_handler_nursery
|
||||||
|
or
|
||||||
|
self._stream_handler_tn
|
||||||
|
),
|
||||||
listen_addrs=accept_addrs,
|
listen_addrs=accept_addrs,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -1145,13 +1149,17 @@ async def open_ipc_server(
|
||||||
|
|
||||||
async with maybe_open_nursery(
|
async with maybe_open_nursery(
|
||||||
nursery=parent_tn,
|
nursery=parent_tn,
|
||||||
) as rent_tn:
|
) as parent_tn:
|
||||||
no_more_peers = trio.Event()
|
no_more_peers = trio.Event()
|
||||||
no_more_peers.set()
|
no_more_peers.set()
|
||||||
|
|
||||||
ipc_server = IPCServer(
|
ipc_server = IPCServer(
|
||||||
_parent_tn=rent_tn,
|
_parent_tn=parent_tn,
|
||||||
_stream_handler_tn=stream_handler_tn or rent_tn,
|
_stream_handler_tn=(
|
||||||
|
stream_handler_tn
|
||||||
|
or
|
||||||
|
parent_tn
|
||||||
|
),
|
||||||
_no_more_peers=no_more_peers,
|
_no_more_peers=no_more_peers,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -23,14 +23,15 @@ considered optional within the context of this runtime-library.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
from multiprocessing import shared_memory as shm
|
||||||
|
from multiprocessing.shared_memory import (
|
||||||
|
# SharedMemory,
|
||||||
|
ShareableList,
|
||||||
|
)
|
||||||
|
import platform
|
||||||
from sys import byteorder
|
from sys import byteorder
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from multiprocessing import shared_memory as shm
|
|
||||||
from multiprocessing.shared_memory import (
|
|
||||||
SharedMemory,
|
|
||||||
ShareableList,
|
|
||||||
)
|
|
||||||
|
|
||||||
from msgspec import (
|
from msgspec import (
|
||||||
Struct,
|
Struct,
|
||||||
|
@ -61,7 +62,7 @@ except ImportError:
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
disable_mantracker()
|
SharedMemory = disable_mantracker()
|
||||||
|
|
||||||
|
|
||||||
class SharedInt:
|
class SharedInt:
|
||||||
|
@ -797,8 +798,15 @@ def open_shm_list(
|
||||||
# "close" attached shm on actor teardown
|
# "close" attached shm on actor teardown
|
||||||
try:
|
try:
|
||||||
actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
|
|
||||||
actor.lifetime_stack.callback(shml.shm.close)
|
actor.lifetime_stack.callback(shml.shm.close)
|
||||||
actor.lifetime_stack.callback(shml.shm.unlink)
|
|
||||||
|
# XXX on 3.13+ we don't need to call this?
|
||||||
|
# -> bc we pass `track=False` for `SharedMemeory` orr?
|
||||||
|
if (
|
||||||
|
platform.python_version_tuple()[:-1] < ('3', '13')
|
||||||
|
):
|
||||||
|
actor.lifetime_stack.callback(shml.shm.unlink)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
log.warning('tractor runtime not active, skipping teardown steps')
|
log.warning('tractor runtime not active, skipping teardown steps')
|
||||||
|
|
||||||
|
|
|
@ -430,6 +430,7 @@ class MsgpackTransport(MsgTransport):
|
||||||
return await self.stream.send_all(size + bytes_data)
|
return await self.stream.send_all(size + bytes_data)
|
||||||
except (
|
except (
|
||||||
trio.BrokenResourceError,
|
trio.BrokenResourceError,
|
||||||
|
trio.ClosedResourceError,
|
||||||
) as _re:
|
) as _re:
|
||||||
trans_err = _re
|
trans_err = _re
|
||||||
tpt_name: str = f'{type(self).__name__!r}'
|
tpt_name: str = f'{type(self).__name__!r}'
|
||||||
|
@ -458,6 +459,22 @@ class MsgpackTransport(MsgTransport):
|
||||||
)
|
)
|
||||||
raise tpt_closed from trans_err
|
raise tpt_closed from trans_err
|
||||||
|
|
||||||
|
# case trio.ClosedResourceError() if (
|
||||||
|
# 'this socket was already closed'
|
||||||
|
# in
|
||||||
|
# trans_err.args[0]
|
||||||
|
# ):
|
||||||
|
# tpt_closed = TransportClosed.from_src_exc(
|
||||||
|
# message=(
|
||||||
|
# f'{tpt_name} already closed by peer\n'
|
||||||
|
# ),
|
||||||
|
# body=f'{self}\n',
|
||||||
|
# src_exc=trans_err,
|
||||||
|
# raise_on_report=True,
|
||||||
|
# loglevel='transport',
|
||||||
|
# )
|
||||||
|
# raise tpt_closed from trans_err
|
||||||
|
|
||||||
# unless the disconnect condition falls under "a
|
# unless the disconnect condition falls under "a
|
||||||
# normal operation breakage" we usualy console warn
|
# normal operation breakage" we usualy console warn
|
||||||
# about it.
|
# about it.
|
||||||
|
|
|
@ -215,7 +215,7 @@ class LinkedTaskChannel(
|
||||||
val: Any = None,
|
val: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Synchronize aio-sde with its trio-parent.
|
Synchronize aio-side with its trio-parent.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self._aio_started_val = val
|
self._aio_started_val = val
|
||||||
|
@ -459,14 +459,22 @@ def _run_asyncio_task(
|
||||||
f'Task exited with final result: {result!r}\n'
|
f'Task exited with final result: {result!r}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
# only close the aio (child) side which will relay
|
# XXX ALWAYS close the child-`asyncio`-task-side's
|
||||||
# a `trio.EndOfChannel` to the trio (parent) side.
|
# `to_trio` handle which will in turn relay
|
||||||
|
# a `trio.EndOfChannel` to the `trio`-parent.
|
||||||
|
# Consequently the parent `trio` task MUST ALWAYS
|
||||||
|
# check for any `chan._aio_err` to be raised when it
|
||||||
|
# receives an EoC.
|
||||||
|
#
|
||||||
|
# NOTE, there are 2 EoC cases,
|
||||||
|
# - normal/graceful EoC due to the aio-side actually
|
||||||
|
# terminating its "streaming", but the task did not
|
||||||
|
# error and is not yet complete.
|
||||||
|
#
|
||||||
|
# - the aio-task terminated and we specially mark the
|
||||||
|
# closure as due to the `asyncio.Task`'s exit.
|
||||||
#
|
#
|
||||||
# XXX NOTE, that trio-side MUST then in such cases
|
|
||||||
# check for a `chan._aio_err` and raise it!!
|
|
||||||
to_trio.close()
|
to_trio.close()
|
||||||
# specially mark the closure as due to the
|
|
||||||
# asyncio.Task terminating!
|
|
||||||
chan._closed_by_aio_task = True
|
chan._closed_by_aio_task = True
|
||||||
|
|
||||||
aio_task_complete.set()
|
aio_task_complete.set()
|
||||||
|
@ -846,8 +854,6 @@ async def translate_aio_errors(
|
||||||
chan._trio_to_raise = aio_err
|
chan._trio_to_raise = aio_err
|
||||||
trio_err = chan._trio_err = eoc
|
trio_err = chan._trio_err = eoc
|
||||||
#
|
#
|
||||||
# await tractor.pause(shield=True)
|
|
||||||
#
|
|
||||||
# ?TODO?, raise something like a,
|
# ?TODO?, raise something like a,
|
||||||
# chan._trio_to_raise = AsyncioErrored()
|
# chan._trio_to_raise = AsyncioErrored()
|
||||||
# BUT, with the tb rewritten to reflect the underlying
|
# BUT, with the tb rewritten to reflect the underlying
|
||||||
|
|
|
@ -116,9 +116,18 @@ async def collapse_eg(
|
||||||
except BaseExceptionGroup as _beg:
|
except BaseExceptionGroup as _beg:
|
||||||
beg = _beg
|
beg = _beg
|
||||||
|
|
||||||
if bp:
|
if (
|
||||||
|
bp
|
||||||
|
and
|
||||||
|
len(beg.exceptions) > 1
|
||||||
|
):
|
||||||
import tractor
|
import tractor
|
||||||
await tractor.pause(shield=True)
|
if tractor.current_actor(
|
||||||
|
err_on_no_runtime=False,
|
||||||
|
):
|
||||||
|
await tractor.pause(shield=True)
|
||||||
|
else:
|
||||||
|
breakpoint()
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(exc := get_collapsed_eg(beg))
|
(exc := get_collapsed_eg(beg))
|
||||||
|
|
|
@ -31,7 +31,6 @@ from typing import (
|
||||||
AsyncIterator,
|
AsyncIterator,
|
||||||
Callable,
|
Callable,
|
||||||
Hashable,
|
Hashable,
|
||||||
Optional,
|
|
||||||
Sequence,
|
Sequence,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
|
@ -204,7 +203,7 @@ class _Cache:
|
||||||
a kept-alive-while-in-use async resource.
|
a kept-alive-while-in-use async resource.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
service_n: Optional[trio.Nursery] = None
|
service_tn: trio.Nursery|None = None
|
||||||
locks: dict[Hashable, trio.Lock] = {}
|
locks: dict[Hashable, trio.Lock] = {}
|
||||||
users: int = 0
|
users: int = 0
|
||||||
values: dict[Any, Any] = {}
|
values: dict[Any, Any] = {}
|
||||||
|
@ -213,7 +212,7 @@ class _Cache:
|
||||||
tuple[trio.Nursery, trio.Event]
|
tuple[trio.Nursery, trio.Event]
|
||||||
] = {}
|
] = {}
|
||||||
# nurseries: dict[int, trio.Nursery] = {}
|
# nurseries: dict[int, trio.Nursery] = {}
|
||||||
no_more_users: Optional[trio.Event] = None
|
no_more_users: trio.Event|None = None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def run_ctx(
|
async def run_ctx(
|
||||||
|
@ -223,18 +222,16 @@ class _Cache:
|
||||||
task_status: trio.TaskStatus[T] = trio.TASK_STATUS_IGNORED,
|
task_status: trio.TaskStatus[T] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
async with mng as value:
|
||||||
async with mng as value:
|
_, no_more_users = cls.resources[ctx_key]
|
||||||
_, no_more_users = cls.resources[ctx_key]
|
cls.values[ctx_key] = value
|
||||||
try:
|
task_status.started(value)
|
||||||
cls.values[ctx_key] = value
|
try:
|
||||||
task_status.started(value)
|
await no_more_users.wait()
|
||||||
await no_more_users.wait()
|
finally:
|
||||||
finally:
|
# discard nursery ref so it won't be re-used (an error)?
|
||||||
value = cls.values.pop(ctx_key)
|
value = cls.values.pop(ctx_key)
|
||||||
finally:
|
cls.resources.pop(ctx_key)
|
||||||
# discard nursery ref so it won't be re-used (an error)?
|
|
||||||
cls.resources.pop(ctx_key)
|
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
@ -296,15 +293,15 @@ async def maybe_open_context(
|
||||||
f'task: {task}\n'
|
f'task: {task}\n'
|
||||||
f'task_tn: {task_tn}\n'
|
f'task_tn: {task_tn}\n'
|
||||||
)
|
)
|
||||||
service_n = tn
|
service_tn = tn
|
||||||
else:
|
else:
|
||||||
service_n: trio.Nursery = current_actor()._service_n
|
service_tn: trio.Nursery = current_actor()._service_tn
|
||||||
|
|
||||||
# TODO: is there any way to allocate
|
# TODO: is there any way to allocate
|
||||||
# a 'stays-open-till-last-task-finshed nursery?
|
# a 'stays-open-till-last-task-finshed nursery?
|
||||||
# service_n: trio.Nursery
|
# service_tn: trio.Nursery
|
||||||
# async with maybe_open_nursery(_Cache.service_n) as service_n:
|
# async with maybe_open_nursery(_Cache.service_tn) as service_tn:
|
||||||
# _Cache.service_n = service_n
|
# _Cache.service_tn = service_tn
|
||||||
|
|
||||||
cache_miss_ke: KeyError|None = None
|
cache_miss_ke: KeyError|None = None
|
||||||
maybe_taskc: trio.Cancelled|None = None
|
maybe_taskc: trio.Cancelled|None = None
|
||||||
|
@ -326,8 +323,8 @@ async def maybe_open_context(
|
||||||
mngr = acm_func(**kwargs)
|
mngr = acm_func(**kwargs)
|
||||||
resources = _Cache.resources
|
resources = _Cache.resources
|
||||||
assert not resources.get(ctx_key), f'Resource exists? {ctx_key}'
|
assert not resources.get(ctx_key), f'Resource exists? {ctx_key}'
|
||||||
resources[ctx_key] = (service_n, trio.Event())
|
resources[ctx_key] = (service_tn, trio.Event())
|
||||||
yielded: Any = await service_n.start(
|
yielded: Any = await service_tn.start(
|
||||||
_Cache.run_ctx,
|
_Cache.run_ctx,
|
||||||
mngr,
|
mngr,
|
||||||
ctx_key,
|
ctx_key,
|
||||||
|
|
Loading…
Reference in New Issue