forked from goodboy/tractor
Merge pull request #128 from goodboy/flaky_tests
Drop trio-run-in-process, use pure trio process spawner, test out of channel ctrl-c subactor cancellationfix_win_ci_again
commit
ed96672136
32
.travis.yml
32
.travis.yml
|
@ -8,6 +8,17 @@ matrix:
|
||||||
os: windows
|
os: windows
|
||||||
language: sh
|
language: sh
|
||||||
python: 3.x # only works on linux
|
python: 3.x # only works on linux
|
||||||
|
env: SPAWN_BACKEND="mp"
|
||||||
|
before_install:
|
||||||
|
- choco install python3 --params "/InstallDir:C:\\Python"
|
||||||
|
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
|
||||||
|
- python -m pip install --upgrade pip wheel
|
||||||
|
|
||||||
|
- name: "Windows, Python Latest: trio"
|
||||||
|
os: windows
|
||||||
|
language: sh
|
||||||
|
python: 3.x # only works on linux
|
||||||
|
env: SPAWN_BACKEND="trio"
|
||||||
before_install:
|
before_install:
|
||||||
- choco install python3 --params "/InstallDir:C:\\Python"
|
- choco install python3 --params "/InstallDir:C:\\Python"
|
||||||
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
|
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
|
||||||
|
@ -16,6 +27,17 @@ matrix:
|
||||||
- name: "Windows, Python 3.7: multiprocessing"
|
- name: "Windows, Python 3.7: multiprocessing"
|
||||||
os: windows
|
os: windows
|
||||||
python: 3.7 # only works on linux
|
python: 3.7 # only works on linux
|
||||||
|
env: SPAWN_BACKEND="mp"
|
||||||
|
language: sh
|
||||||
|
before_install:
|
||||||
|
- choco install python3 --version 3.7.4 --params "/InstallDir:C:\\Python"
|
||||||
|
- export PATH="/c/Python:/c/Python/Scripts:$PATH"
|
||||||
|
- python -m pip install --upgrade pip wheel
|
||||||
|
|
||||||
|
- name: "Windows, Python 3.7: trio"
|
||||||
|
os: windows
|
||||||
|
python: 3.7 # only works on linux
|
||||||
|
env: SPAWN_BACKEND="trio"
|
||||||
language: sh
|
language: sh
|
||||||
before_install:
|
before_install:
|
||||||
- choco install python3 --version 3.7.4 --params "/InstallDir:C:\\Python"
|
- choco install python3 --version 3.7.4 --params "/InstallDir:C:\\Python"
|
||||||
|
@ -25,16 +47,16 @@ matrix:
|
||||||
- name: "Python 3.7: multiprocessing"
|
- name: "Python 3.7: multiprocessing"
|
||||||
python: 3.7 # this works for Linux but is ignored on macOS or Windows
|
python: 3.7 # this works for Linux but is ignored on macOS or Windows
|
||||||
env: SPAWN_BACKEND="mp"
|
env: SPAWN_BACKEND="mp"
|
||||||
- name: "Python 3.7: trio-run-in-process"
|
- name: "Python 3.7: trio"
|
||||||
python: 3.7 # this works for Linux but is ignored on macOS or Windows
|
python: 3.7 # this works for Linux but is ignored on macOS or Windows
|
||||||
env: SPAWN_BACKEND="trio_run_in_process"
|
env: SPAWN_BACKEND="trio"
|
||||||
|
|
||||||
- name: "Python 3.8: multiprocessing"
|
- name: "Python 3.8: multiprocessing"
|
||||||
python: 3.8 # this works for Linux but is ignored on macOS or Windows
|
python: 3.8 # this works for Linux but is ignored on macOS or Windows
|
||||||
env: SPAWN_BACKEND="mp"
|
env: SPAWN_BACKEND="mp"
|
||||||
- name: "Python 3.8: trio-run-in-process"
|
- name: "Python 3.8: trio"
|
||||||
python: 3.8 # this works for Linux but is ignored on macOS or Windows
|
python: 3.8 # this works for Linux but is ignored on macOS or Windows
|
||||||
env: SPAWN_BACKEND="trio_run_in_process"
|
env: SPAWN_BACKEND="trio"
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- cd $TRAVIS_BUILD_DIR
|
- cd $TRAVIS_BUILD_DIR
|
||||||
|
@ -43,4 +65,4 @@ install:
|
||||||
|
|
||||||
script:
|
script:
|
||||||
- mypy tractor/ --ignore-missing-imports
|
- mypy tractor/ --ignore-missing-imports
|
||||||
- pytest tests/ --no-print-logs --spawn-backend=${SPAWN_BACKEND}
|
- pytest tests/ --spawn-backend=${SPAWN_BACKEND}
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -39,7 +39,7 @@ setup(
|
||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'msgpack', 'trio>0.8', 'async_generator', 'colorlog', 'wrapt',
|
'msgpack', 'trio>0.8', 'async_generator', 'colorlog', 'wrapt',
|
||||||
'trio_typing', 'trio-run-in-process',
|
'trio_typing', 'cloudpickle',
|
||||||
],
|
],
|
||||||
tests_require=['pytest'],
|
tests_require=['pytest'],
|
||||||
python_requires=">=3.7",
|
python_requires=">=3.7",
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
"""
|
"""
|
||||||
``tractor`` testing!!
|
``tractor`` testing!!
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tractor
|
import tractor
|
||||||
from tractor.testing import tractor_test
|
|
||||||
|
# export for tests
|
||||||
|
from tractor.testing import tractor_test # noqa
|
||||||
|
|
||||||
|
|
||||||
pytest_plugins = ['pytester']
|
pytest_plugins = ['pytester']
|
||||||
_arb_addr = '127.0.0.1', random.randint(1000, 9999)
|
_arb_addr = '127.0.0.1', random.randint(1000, 9999)
|
||||||
|
|
||||||
|
|
||||||
|
no_windows = pytest.mark.skipif(
|
||||||
|
platform.system() == "Windows",
|
||||||
|
reason="Test is unsupported on windows",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser):
|
def pytest_addoption(parser):
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--ll", action="store", dest='loglevel',
|
"--ll", action="store", dest='loglevel',
|
||||||
|
@ -21,7 +30,7 @@ def pytest_addoption(parser):
|
||||||
|
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--spawn-backend", action="store", dest='spawn_backend',
|
"--spawn-backend", action="store", dest='spawn_backend',
|
||||||
default='trio_run_in_process',
|
default='trio',
|
||||||
help="Processing spawning backend to use for test run",
|
help="Processing spawning backend to use for test run",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,12 +38,9 @@ def pytest_addoption(parser):
|
||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
backend = config.option.spawn_backend
|
backend = config.option.spawn_backend
|
||||||
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
backend = 'mp'
|
|
||||||
|
|
||||||
if backend == 'mp':
|
if backend == 'mp':
|
||||||
tractor._spawn.try_set_start_method('spawn')
|
tractor._spawn.try_set_start_method('spawn')
|
||||||
elif backend == 'trio_run_in_process':
|
elif backend == 'trio':
|
||||||
tractor._spawn.try_set_start_method(backend)
|
tractor._spawn.try_set_start_method(backend)
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,6 +52,18 @@ def loglevel(request):
|
||||||
tractor.log._default_loglevel = orig
|
tractor.log._default_loglevel = orig
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def spawn_backend(request):
|
||||||
|
return request.config.option.spawn_backend
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def travis():
|
||||||
|
"""Bool determining whether running inside TravisCI.
|
||||||
|
"""
|
||||||
|
return os.environ.get('TRAVIS', False)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
@pytest.fixture(scope='session')
|
||||||
def arb_addr():
|
def arb_addr():
|
||||||
return _arb_addr
|
return _arb_addr
|
||||||
|
@ -56,7 +74,7 @@ def pytest_generate_tests(metafunc):
|
||||||
if not spawn_backend:
|
if not spawn_backend:
|
||||||
# XXX some weird windows bug with `pytest`?
|
# XXX some weird windows bug with `pytest`?
|
||||||
spawn_backend = 'mp'
|
spawn_backend = 'mp'
|
||||||
assert spawn_backend in ('mp', 'trio_run_in_process')
|
assert spawn_backend in ('mp', 'trio')
|
||||||
|
|
||||||
if 'start_method' in metafunc.fixturenames:
|
if 'start_method' in metafunc.fixturenames:
|
||||||
if spawn_backend == 'mp':
|
if spawn_backend == 'mp':
|
||||||
|
@ -67,11 +85,7 @@ def pytest_generate_tests(metafunc):
|
||||||
# removing XXX: the fork method is in general
|
# removing XXX: the fork method is in general
|
||||||
# incompatible with trio's global scheduler state
|
# incompatible with trio's global scheduler state
|
||||||
methods.remove('fork')
|
methods.remove('fork')
|
||||||
elif spawn_backend == 'trio_run_in_process':
|
elif spawn_backend == 'trio':
|
||||||
if platform.system() == "Windows":
|
methods = ['trio']
|
||||||
pytest.fail(
|
|
||||||
"Only `--spawn-backend=mp` is supported on Windows")
|
|
||||||
|
|
||||||
methods = ['trio_run_in_process']
|
|
||||||
|
|
||||||
metafunc.parametrize("start_method", methods, scope='module')
|
metafunc.parametrize("start_method", methods, scope='module')
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"""
|
"""
|
||||||
Cancellation and error propagation
|
Cancellation and error propagation
|
||||||
"""
|
"""
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
import platform
|
import platform
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
|
|
||||||
|
@ -8,7 +10,7 @@ import pytest
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
||||||
from conftest import tractor_test
|
from conftest import tractor_test, no_windows
|
||||||
|
|
||||||
|
|
||||||
async def assert_err(delay=0):
|
async def assert_err(delay=0):
|
||||||
|
@ -17,7 +19,7 @@ async def assert_err(delay=0):
|
||||||
|
|
||||||
|
|
||||||
async def sleep_forever():
|
async def sleep_forever():
|
||||||
await trio.sleep(float('inf'))
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
|
||||||
async def do_nuthin():
|
async def do_nuthin():
|
||||||
|
@ -118,7 +120,8 @@ def do_nothing():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_cancel_single_subactor(arb_addr):
|
@pytest.mark.parametrize('mechanism', ['nursery_cancel', KeyboardInterrupt])
|
||||||
|
def test_cancel_single_subactor(arb_addr, mechanism):
|
||||||
"""Ensure a ``ActorNursery.start_actor()`` spawned subactor
|
"""Ensure a ``ActorNursery.start_actor()`` spawned subactor
|
||||||
cancels when the nursery is cancelled.
|
cancels when the nursery is cancelled.
|
||||||
"""
|
"""
|
||||||
|
@ -132,10 +135,17 @@ def test_cancel_single_subactor(arb_addr):
|
||||||
)
|
)
|
||||||
assert (await portal.run(__name__, 'do_nothing')) is None
|
assert (await portal.run(__name__, 'do_nothing')) is None
|
||||||
|
|
||||||
# would hang otherwise
|
if mechanism == 'nursery_cancel':
|
||||||
await nursery.cancel()
|
# would hang otherwise
|
||||||
|
await nursery.cancel()
|
||||||
|
else:
|
||||||
|
raise mechanism
|
||||||
|
|
||||||
tractor.run(spawn_actor, arbiter_addr=arb_addr)
|
if mechanism == 'nursery_cancel':
|
||||||
|
tractor.run(spawn_actor, arbiter_addr=arb_addr)
|
||||||
|
else:
|
||||||
|
with pytest.raises(mechanism):
|
||||||
|
tractor.run(spawn_actor, arbiter_addr=arb_addr)
|
||||||
|
|
||||||
|
|
||||||
async def stream_forever():
|
async def stream_forever():
|
||||||
|
@ -153,7 +163,7 @@ async def test_cancel_infinite_streamer(start_method):
|
||||||
with trio.move_on_after(1) as cancel_scope:
|
with 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(
|
||||||
f'donny',
|
'donny',
|
||||||
rpc_module_paths=[__name__],
|
rpc_module_paths=[__name__],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -197,7 +207,7 @@ async def test_cancel_infinite_streamer(start_method):
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_some_cancels_all(num_actors_and_errs, start_method):
|
async def test_some_cancels_all(num_actors_and_errs, start_method, loglevel):
|
||||||
"""Verify a subset of failed subactors causes all others in
|
"""Verify a subset of failed subactors causes all others in
|
||||||
the nursery to be cancelled just like the strategy in trio.
|
the nursery to be cancelled just like the strategy in trio.
|
||||||
|
|
||||||
|
@ -289,7 +299,7 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
This test goes only 2 nurseries deep but we should eventually have tests
|
This test goes only 2 nurseries deep but we should eventually have tests
|
||||||
for arbitrary n-depth actor trees.
|
for arbitrary n-depth actor trees.
|
||||||
"""
|
"""
|
||||||
if start_method == 'trio_run_in_process':
|
if start_method == 'trio':
|
||||||
depth = 3
|
depth = 3
|
||||||
subactor_breadth = 2
|
subactor_breadth = 2
|
||||||
else:
|
else:
|
||||||
|
@ -299,7 +309,7 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
# hangs and broken pipes all over the place...
|
# hangs and broken pipes all over the place...
|
||||||
if start_method == 'forkserver':
|
if start_method == 'forkserver':
|
||||||
pytest.skip("Forksever sux hard at nested spawning...")
|
pytest.skip("Forksever sux hard at nested spawning...")
|
||||||
depth = 2
|
depth = 1 # means an additional actor tree of spawning (2 levels deep)
|
||||||
subactor_breadth = 2
|
subactor_breadth = 2
|
||||||
|
|
||||||
with trio.fail_after(120):
|
with trio.fail_after(120):
|
||||||
|
@ -315,10 +325,29 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
except trio.MultiError as err:
|
except trio.MultiError as err:
|
||||||
assert len(err.exceptions) == subactor_breadth
|
assert len(err.exceptions) == subactor_breadth
|
||||||
for subexc in err.exceptions:
|
for subexc in err.exceptions:
|
||||||
assert isinstance(subexc, tractor.RemoteActorError)
|
|
||||||
if depth > 1 and subactor_breadth > 1:
|
|
||||||
|
|
||||||
|
# verify first level actor errors are wrapped as remote
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
|
||||||
|
# windows is often too slow and cancellation seems
|
||||||
|
# to happen before an actor is spawned
|
||||||
|
if subexc is trio.Cancelled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# on windows it seems we can't exactly be sure wtf
|
||||||
|
# will happen..
|
||||||
|
assert subexc.type in (
|
||||||
|
tractor.RemoteActorError,
|
||||||
|
trio.Cancelled,
|
||||||
|
trio.MultiError
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
assert isinstance(subexc, tractor.RemoteActorError)
|
||||||
|
|
||||||
|
if depth > 0 and subactor_breadth > 1:
|
||||||
# XXX not sure what's up with this..
|
# XXX not sure what's up with this..
|
||||||
|
# on windows sometimes spawning is just too slow and
|
||||||
|
# we get back the (sent) cancel signal instead
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
assert (subexc.type is trio.MultiError) or (
|
assert (subexc.type is trio.MultiError) or (
|
||||||
subexc.type is tractor.RemoteActorError)
|
subexc.type is tractor.RemoteActorError)
|
||||||
|
@ -327,3 +356,50 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
else:
|
else:
|
||||||
assert (subexc.type is tractor.RemoteActorError) or (
|
assert (subexc.type is tractor.RemoteActorError) or (
|
||||||
subexc.type is trio.Cancelled)
|
subexc.type is trio.Cancelled)
|
||||||
|
|
||||||
|
|
||||||
|
@no_windows
|
||||||
|
def test_cancel_via_SIGINT(loglevel, start_method):
|
||||||
|
"""Ensure that a control-C (SIGINT) signal cancels both the parent and
|
||||||
|
child processes in trionic fashion
|
||||||
|
"""
|
||||||
|
pid = os.getpid()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
with trio.fail_after(2):
|
||||||
|
async with tractor.open_nursery() as tn:
|
||||||
|
await tn.start_actor('sucka')
|
||||||
|
os.kill(pid, signal.SIGINT)
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
with pytest.raises(KeyboardInterrupt):
|
||||||
|
tractor.run(main)
|
||||||
|
|
||||||
|
|
||||||
|
@no_windows
|
||||||
|
def test_cancel_via_SIGINT_other_task(
|
||||||
|
loglevel,
|
||||||
|
start_method
|
||||||
|
):
|
||||||
|
"""Ensure that a control-C (SIGINT) signal cancels both the parent
|
||||||
|
and child processes in trionic fashion even a subprocess is started
|
||||||
|
from a seperate ``trio`` child task.
|
||||||
|
"""
|
||||||
|
pid = os.getpid()
|
||||||
|
|
||||||
|
async def spawn_and_sleep_forever(task_status=trio.TASK_STATUS_IGNORED):
|
||||||
|
async with tractor.open_nursery() as tn:
|
||||||
|
for i in range(3):
|
||||||
|
await tn.run_in_actor('sucka', sleep_forever)
|
||||||
|
task_status.started()
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# should never timeout since SIGINT should cancel the current program
|
||||||
|
with trio.fail_after(2):
|
||||||
|
async with trio.open_nursery() as n:
|
||||||
|
await n.start(spawn_and_sleep_forever)
|
||||||
|
os.kill(pid, signal.SIGINT)
|
||||||
|
|
||||||
|
with pytest.raises(KeyboardInterrupt):
|
||||||
|
tractor.run(main)
|
||||||
|
|
|
@ -203,7 +203,7 @@ async def cancel_after(wait):
|
||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def time_quad_ex(arb_addr):
|
def time_quad_ex(arb_addr):
|
||||||
timeout = 7 if platform.system() == 'Windows' else 3
|
timeout = 7 if platform.system() == 'Windows' else 4
|
||||||
start = time.time()
|
start = time.time()
|
||||||
results = tractor.run(cancel_after, timeout, arbiter_addr=arb_addr)
|
results = tractor.run(cancel_after, timeout, arbiter_addr=arb_addr)
|
||||||
diff = time.time() - start
|
diff = time.time() - start
|
||||||
|
@ -211,8 +211,12 @@ def time_quad_ex(arb_addr):
|
||||||
return results, diff
|
return results, diff
|
||||||
|
|
||||||
|
|
||||||
def test_a_quadruple_example(time_quad_ex):
|
def test_a_quadruple_example(time_quad_ex, travis, spawn_backend):
|
||||||
"""This also serves as a kind of "we'd like to be this fast test"."""
|
"""This also serves as a kind of "we'd like to be this fast test"."""
|
||||||
|
if travis and spawn_backend == 'mp' and not platform.system() == 'Windows':
|
||||||
|
# no idea, but the travis, mp, linux runs are flaking out here often
|
||||||
|
pytest.skip("Test is too flaky on mp in CI")
|
||||||
|
|
||||||
results, diff = time_quad_ex
|
results, diff = time_quad_ex
|
||||||
assert results
|
assert results
|
||||||
this_fast = 6 if platform.system() == 'Windows' else 2.5
|
this_fast = 6 if platform.system() == 'Windows' else 2.5
|
||||||
|
@ -223,10 +227,16 @@ def test_a_quadruple_example(time_quad_ex):
|
||||||
'cancel_delay',
|
'cancel_delay',
|
||||||
list(map(lambda i: i/10, range(3, 9)))
|
list(map(lambda i: i/10, range(3, 9)))
|
||||||
)
|
)
|
||||||
def test_not_fast_enough_quad(arb_addr, time_quad_ex, cancel_delay):
|
def test_not_fast_enough_quad(
|
||||||
|
arb_addr, time_quad_ex, cancel_delay, travis, spawn_backend
|
||||||
|
):
|
||||||
"""Verify we can cancel midway through the quad example and all actors
|
"""Verify we can cancel midway through the quad example and all actors
|
||||||
cancel gracefully.
|
cancel gracefully.
|
||||||
"""
|
"""
|
||||||
|
if travis and spawn_backend == 'mp' and not platform.system() == 'Windows':
|
||||||
|
# no idea, but the travis, mp, linux runs are flaking out here often
|
||||||
|
pytest.skip("Test is too flaky on mp in CI")
|
||||||
|
|
||||||
results, diff = time_quad_ex
|
results, diff = time_quad_ex
|
||||||
delay = max(diff - cancel_delay, 0)
|
delay = max(diff - cancel_delay, 0)
|
||||||
results = tractor.run(cancel_after, delay, arbiter_addr=arb_addr)
|
results = tractor.run(cancel_after, delay, arbiter_addr=arb_addr)
|
||||||
|
|
|
@ -20,7 +20,7 @@ from async_generator import aclosing
|
||||||
|
|
||||||
from ._ipc import Channel
|
from ._ipc import Channel
|
||||||
from ._streaming import Context, _context
|
from ._streaming import Context, _context
|
||||||
from .log import get_console_log, get_logger
|
from .log import get_logger
|
||||||
from ._exceptions import (
|
from ._exceptions import (
|
||||||
pack_error,
|
pack_error,
|
||||||
unpack_error,
|
unpack_error,
|
||||||
|
@ -149,7 +149,7 @@ async def _invoke(
|
||||||
f"Task {func} was likely cancelled before it was started")
|
f"Task {func} was likely cancelled before it was started")
|
||||||
|
|
||||||
if not actor._rpc_tasks:
|
if not actor._rpc_tasks:
|
||||||
log.info(f"All RPC tasks have completed")
|
log.info("All RPC tasks have completed")
|
||||||
actor._ongoing_rpc_tasks.set()
|
actor._ongoing_rpc_tasks.set()
|
||||||
|
|
||||||
|
|
||||||
|
@ -256,7 +256,7 @@ class Actor:
|
||||||
code (if it exists).
|
code (if it exists).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self._spawn_method == 'trio_run_in_process':
|
if self._spawn_method == 'trio':
|
||||||
parent_data = self._parent_main_data
|
parent_data = self._parent_main_data
|
||||||
if 'init_main_from_name' in parent_data:
|
if 'init_main_from_name' in parent_data:
|
||||||
_mp_fixup_main._fixup_main_from_name(
|
_mp_fixup_main._fixup_main_from_name(
|
||||||
|
@ -339,7 +339,7 @@ class Actor:
|
||||||
|
|
||||||
if not self._peers: # no more channels connected
|
if not self._peers: # no more channels connected
|
||||||
self._no_more_peers.set()
|
self._no_more_peers.set()
|
||||||
log.debug(f"Signalling no more peer channels")
|
log.debug("Signalling no more peer channels")
|
||||||
|
|
||||||
# # XXX: is this necessary (GC should do it?)
|
# # XXX: is this necessary (GC should do it?)
|
||||||
if chan.connected():
|
if chan.connected():
|
||||||
|
@ -539,58 +539,6 @@ class Actor:
|
||||||
f"Exiting msg loop for {chan} from {chan.uid} "
|
f"Exiting msg loop for {chan} from {chan.uid} "
|
||||||
f"with last msg:\n{msg}")
|
f"with last msg:\n{msg}")
|
||||||
|
|
||||||
def _mp_main(
|
|
||||||
self,
|
|
||||||
accept_addr: Tuple[str, int],
|
|
||||||
forkserver_info: Tuple[Any, Any, Any, Any, Any],
|
|
||||||
start_method: str,
|
|
||||||
parent_addr: Tuple[str, int] = None
|
|
||||||
) -> None:
|
|
||||||
"""The routine called *after fork* which invokes a fresh ``trio.run``
|
|
||||||
"""
|
|
||||||
self._forkserver_info = forkserver_info
|
|
||||||
from ._spawn import try_set_start_method
|
|
||||||
spawn_ctx = try_set_start_method(start_method)
|
|
||||||
|
|
||||||
if self.loglevel is not None:
|
|
||||||
log.info(
|
|
||||||
f"Setting loglevel for {self.uid} to {self.loglevel}")
|
|
||||||
get_console_log(self.loglevel)
|
|
||||||
|
|
||||||
assert spawn_ctx
|
|
||||||
log.info(
|
|
||||||
f"Started new {spawn_ctx.current_process()} for {self.uid}")
|
|
||||||
|
|
||||||
_state._current_actor = self
|
|
||||||
|
|
||||||
log.debug(f"parent_addr is {parent_addr}")
|
|
||||||
try:
|
|
||||||
trio.run(partial(
|
|
||||||
self._async_main, accept_addr, parent_addr=parent_addr))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass # handle it the same way trio does?
|
|
||||||
log.info(f"Actor {self.uid} terminated")
|
|
||||||
|
|
||||||
async def _trip_main(
|
|
||||||
self,
|
|
||||||
accept_addr: Tuple[str, int],
|
|
||||||
parent_addr: Tuple[str, int] = None
|
|
||||||
) -> None:
|
|
||||||
"""Entry point for a `trio_run_in_process` subactor.
|
|
||||||
|
|
||||||
Here we don't need to call `trio.run()` since trip does that as
|
|
||||||
part of its subprocess startup sequence.
|
|
||||||
"""
|
|
||||||
if self.loglevel is not None:
|
|
||||||
log.info(
|
|
||||||
f"Setting loglevel for {self.uid} to {self.loglevel}")
|
|
||||||
get_console_log(self.loglevel)
|
|
||||||
|
|
||||||
log.info(f"Started new TRIP process for {self.uid}")
|
|
||||||
_state._current_actor = self
|
|
||||||
await self._async_main(accept_addr, parent_addr=parent_addr)
|
|
||||||
log.info(f"Actor {self.uid} terminated")
|
|
||||||
|
|
||||||
async def _async_main(
|
async def _async_main(
|
||||||
self,
|
self,
|
||||||
accept_addr: Tuple[str, int],
|
accept_addr: Tuple[str, int],
|
||||||
|
@ -661,9 +609,18 @@ class Actor:
|
||||||
# killed (i.e. this actor is cancelled or signalled by the parent)
|
# killed (i.e. this actor is cancelled or signalled by the parent)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
if not registered_with_arbiter:
|
if not registered_with_arbiter:
|
||||||
|
# TODO: I guess we could try to connect back
|
||||||
|
# to the parent through a channel and engage a debugger
|
||||||
|
# once we have that all working with std streams locking?
|
||||||
log.exception(
|
log.exception(
|
||||||
f"Actor errored and failed to register with arbiter "
|
f"Actor errored and failed to register with arbiter "
|
||||||
f"@ {arbiter_addr}")
|
f"@ {arbiter_addr}?")
|
||||||
|
log.error(
|
||||||
|
"\n\n\t^^^ THIS IS PROBABLY A TRACTOR BUGGGGG!!! ^^^\n"
|
||||||
|
"\tCALMLY CALL THE AUTHORITIES AND HIDE YOUR CHILDREN.\n\n"
|
||||||
|
"\tYOUR PARENT CODE IS GOING TO KEEP WORKING FINE!!!\n"
|
||||||
|
"\tTHIS IS HOW RELIABlE SYSTEMS ARE SUPPOSED TO WORK!?!?\n"
|
||||||
|
)
|
||||||
|
|
||||||
if self._parent_chan:
|
if self._parent_chan:
|
||||||
try:
|
try:
|
||||||
|
@ -681,6 +638,7 @@ class Actor:
|
||||||
# XXX wait, why?
|
# XXX wait, why?
|
||||||
# causes a hang if I always raise..
|
# causes a hang if I always raise..
|
||||||
# A parent process does something weird here?
|
# A parent process does something weird here?
|
||||||
|
# i'm so lost now..
|
||||||
raise
|
raise
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
@ -695,7 +653,7 @@ class Actor:
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Waiting for remaining peers {self._peers} to clear")
|
f"Waiting for remaining peers {self._peers} to clear")
|
||||||
await self._no_more_peers.wait()
|
await self._no_more_peers.wait()
|
||||||
log.debug(f"All peer channels are complete")
|
log.debug("All peer channels are complete")
|
||||||
|
|
||||||
# tear down channel server no matter what since we errored
|
# tear down channel server no matter what since we errored
|
||||||
# or completed
|
# or completed
|
||||||
|
@ -729,8 +687,8 @@ class Actor:
|
||||||
port=accept_port, host=accept_host,
|
port=accept_port, host=accept_host,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
log.debug(f"Started tcp server(s) on"
|
log.debug("Started tcp server(s) on" # type: ignore
|
||||||
" {[l.socket for l in listeners]}") # type: ignore
|
f" {[l.socket for l in listeners]}")
|
||||||
self._listeners.extend(listeners)
|
self._listeners.extend(listeners)
|
||||||
task_status.started()
|
task_status.started()
|
||||||
|
|
||||||
|
@ -917,7 +875,7 @@ async def _start_actor(
|
||||||
port: int,
|
port: int,
|
||||||
arbiter_addr: Tuple[str, int],
|
arbiter_addr: Tuple[str, int],
|
||||||
nursery: trio.Nursery = None
|
nursery: trio.Nursery = None
|
||||||
):
|
) -> Any:
|
||||||
"""Spawn a local actor by starting a task to execute it's main async
|
"""Spawn a local actor by starting a task to execute it's main async
|
||||||
function.
|
function.
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import sys
|
||||||
|
import trio
|
||||||
|
import cloudpickle
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
trio.run(cloudpickle.load(sys.stdin.buffer))
|
|
@ -0,0 +1,74 @@
|
||||||
|
"""
|
||||||
|
Process entry points.
|
||||||
|
"""
|
||||||
|
from functools import partial
|
||||||
|
from typing import Tuple, Any
|
||||||
|
|
||||||
|
import trio # type: ignore
|
||||||
|
|
||||||
|
from ._actor import Actor
|
||||||
|
from .log import get_console_log, get_logger
|
||||||
|
from . import _state
|
||||||
|
|
||||||
|
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _mp_main(
|
||||||
|
actor: 'Actor',
|
||||||
|
accept_addr: Tuple[str, int],
|
||||||
|
forkserver_info: Tuple[Any, Any, Any, Any, Any],
|
||||||
|
start_method: str,
|
||||||
|
parent_addr: Tuple[str, int] = None,
|
||||||
|
) -> None:
|
||||||
|
"""The routine called *after fork* which invokes a fresh ``trio.run``
|
||||||
|
"""
|
||||||
|
actor._forkserver_info = forkserver_info
|
||||||
|
from ._spawn import try_set_start_method
|
||||||
|
spawn_ctx = try_set_start_method(start_method)
|
||||||
|
|
||||||
|
if actor.loglevel is not None:
|
||||||
|
log.info(
|
||||||
|
f"Setting loglevel for {actor.uid} to {actor.loglevel}")
|
||||||
|
get_console_log(actor.loglevel)
|
||||||
|
|
||||||
|
assert spawn_ctx
|
||||||
|
log.info(
|
||||||
|
f"Started new {spawn_ctx.current_process()} for {actor.uid}")
|
||||||
|
|
||||||
|
_state._current_actor = actor
|
||||||
|
|
||||||
|
log.debug(f"parent_addr is {parent_addr}")
|
||||||
|
trio_main = partial(
|
||||||
|
actor._async_main,
|
||||||
|
accept_addr,
|
||||||
|
parent_addr=parent_addr
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
trio.run(trio_main)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass # handle it the same way trio does?
|
||||||
|
log.info(f"Actor {actor.uid} terminated")
|
||||||
|
|
||||||
|
|
||||||
|
async def _trio_main(
|
||||||
|
actor: 'Actor',
|
||||||
|
accept_addr: Tuple[str, int],
|
||||||
|
parent_addr: Tuple[str, int] = None
|
||||||
|
) -> None:
|
||||||
|
"""Entry point for a `trio_run_in_process` subactor.
|
||||||
|
|
||||||
|
Here we don't need to call `trio.run()` since trip does that as
|
||||||
|
part of its subprocess startup sequence.
|
||||||
|
"""
|
||||||
|
if actor.loglevel is not None:
|
||||||
|
log.info(
|
||||||
|
f"Setting loglevel for {actor.uid} to {actor.loglevel}")
|
||||||
|
get_console_log(actor.loglevel)
|
||||||
|
|
||||||
|
log.info(f"Started new trio process for {actor.uid}")
|
||||||
|
|
||||||
|
_state._current_actor = actor
|
||||||
|
|
||||||
|
await actor._async_main(accept_addr, parent_addr=parent_addr)
|
||||||
|
log.info(f"Actor {actor.uid} terminated")
|
|
@ -1,14 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Machinery for actor process spawning using multiple backends.
|
Machinery for actor process spawning using multiple backends.
|
||||||
"""
|
"""
|
||||||
|
import sys
|
||||||
import inspect
|
import inspect
|
||||||
|
import subprocess
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import platform
|
import platform
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
import cloudpickle
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
from async_generator import aclosing
|
from async_generator import aclosing, asynccontextmanager
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from multiprocessing import semaphore_tracker # type: ignore
|
from multiprocessing import semaphore_tracker # type: ignore
|
||||||
|
@ -26,6 +30,7 @@ from ._state import current_actor
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
from ._portal import Portal
|
from ._portal import Portal
|
||||||
from ._actor import Actor, ActorFailure
|
from ._actor import Actor, ActorFailure
|
||||||
|
from ._entry import _mp_main, _trio_main
|
||||||
|
|
||||||
|
|
||||||
log = get_logger('tractor')
|
log = get_logger('tractor')
|
||||||
|
@ -40,23 +45,23 @@ if platform.system() == 'Windows':
|
||||||
_ctx = mp.get_context("spawn")
|
_ctx = mp.get_context("spawn")
|
||||||
|
|
||||||
async def proc_waiter(proc: mp.Process) -> None:
|
async def proc_waiter(proc: mp.Process) -> None:
|
||||||
await trio.hazmat.WaitForSingleObject(proc.sentinel)
|
await trio.lowlevel.WaitForSingleObject(proc.sentinel)
|
||||||
else:
|
else:
|
||||||
# *NIX systems use ``trio_run_in_process` as our default (for now)
|
# *NIX systems use ``trio`` primitives as our default
|
||||||
import trio_run_in_process
|
_spawn_method = "trio"
|
||||||
_spawn_method = "trio_run_in_process"
|
|
||||||
|
|
||||||
async def proc_waiter(proc: mp.Process) -> None:
|
async def proc_waiter(proc: mp.Process) -> None:
|
||||||
await trio.hazmat.wait_readable(proc.sentinel)
|
await trio.lowlevel.wait_readable(proc.sentinel)
|
||||||
|
|
||||||
|
|
||||||
def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
|
def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
|
||||||
"""Attempt to set the start method for process starting, aka the "actor
|
"""Attempt to set the method for process starting, aka the "actor
|
||||||
spawning backend".
|
spawning backend".
|
||||||
|
|
||||||
If the desired method is not supported this function will error. On
|
If the desired method is not supported this function will error.
|
||||||
Windows the only supported option is the ``multiprocessing`` "spawn"
|
On Windows only the ``multiprocessing`` "spawn" method is offered
|
||||||
method. The default on *nix systems is ``trio_run_in_process``.
|
besides the default ``trio`` which uses async wrapping around
|
||||||
|
``subprocess.Popen``.
|
||||||
"""
|
"""
|
||||||
global _ctx
|
global _ctx
|
||||||
global _spawn_method
|
global _spawn_method
|
||||||
|
@ -66,9 +71,8 @@ def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
|
||||||
# forking is incompatible with ``trio``s global task tree
|
# forking is incompatible with ``trio``s global task tree
|
||||||
methods.remove('fork')
|
methods.remove('fork')
|
||||||
|
|
||||||
# no Windows support for trip yet
|
# supported on all platforms
|
||||||
if platform.system() != 'Windows':
|
methods += ['trio']
|
||||||
methods += ['trio_run_in_process']
|
|
||||||
|
|
||||||
if name not in methods:
|
if name not in methods:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -77,7 +81,7 @@ def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
|
||||||
elif name == 'forkserver':
|
elif name == 'forkserver':
|
||||||
_forkserver_override.override_stdlib()
|
_forkserver_override.override_stdlib()
|
||||||
_ctx = mp.get_context(name)
|
_ctx = mp.get_context(name)
|
||||||
elif name == 'trio_run_in_process':
|
elif name == 'trio':
|
||||||
_ctx = None
|
_ctx = None
|
||||||
else:
|
else:
|
||||||
_ctx = mp.get_context(name)
|
_ctx = mp.get_context(name)
|
||||||
|
@ -118,6 +122,7 @@ async def exhaust_portal(
|
||||||
# we reraise in the parent task via a ``trio.MultiError``
|
# we reraise in the parent task via a ``trio.MultiError``
|
||||||
return err
|
return err
|
||||||
else:
|
else:
|
||||||
|
log.debug(f"Returning final result: {final}")
|
||||||
return final
|
return final
|
||||||
|
|
||||||
|
|
||||||
|
@ -152,6 +157,29 @@ async def cancel_on_completion(
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def run_in_process(subactor, async_fn, *args, **kwargs):
|
||||||
|
encoded_job = cloudpickle.dumps(partial(async_fn, *args, **kwargs))
|
||||||
|
|
||||||
|
async with await trio.open_process(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
"-m",
|
||||||
|
# Hardcode this (instead of using ``_child.__name__`` to avoid a
|
||||||
|
# double import warning: https://stackoverflow.com/a/45070583
|
||||||
|
"tractor._child",
|
||||||
|
# This is merely an identifier for debugging purposes when
|
||||||
|
# viewing the process tree from the OS
|
||||||
|
str(subactor.uid),
|
||||||
|
],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
) as proc:
|
||||||
|
|
||||||
|
# send func object to call in child
|
||||||
|
await proc.stdin.send_all(encoded_job)
|
||||||
|
yield proc
|
||||||
|
|
||||||
|
|
||||||
async def new_proc(
|
async def new_proc(
|
||||||
name: str,
|
name: str,
|
||||||
actor_nursery: 'ActorNursery', # type: ignore
|
actor_nursery: 'ActorNursery', # type: ignore
|
||||||
|
@ -172,10 +200,11 @@ async def new_proc(
|
||||||
subactor._spawn_method = _spawn_method
|
subactor._spawn_method = _spawn_method
|
||||||
|
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
if use_trio_run_in_process or _spawn_method == 'trio_run_in_process':
|
if use_trio_run_in_process or _spawn_method == 'trio':
|
||||||
# trio_run_in_process
|
async with run_in_process(
|
||||||
async with trio_run_in_process.open_in_process(
|
subactor,
|
||||||
subactor._trip_main,
|
_trio_main,
|
||||||
|
subactor,
|
||||||
bind_addr,
|
bind_addr,
|
||||||
parent_addr,
|
parent_addr,
|
||||||
) as proc:
|
) as proc:
|
||||||
|
@ -198,7 +227,10 @@ async def new_proc(
|
||||||
cancel_scope = await nursery.start(
|
cancel_scope = await nursery.start(
|
||||||
cancel_on_completion, portal, subactor, errors)
|
cancel_on_completion, portal, subactor, errors)
|
||||||
|
|
||||||
# TRIP blocks here until process is complete
|
# Wait for proc termination but **dont'** yet call
|
||||||
|
# ``trio.Process.__aexit__()`` (it tears down stdio
|
||||||
|
# which will kill any waiting remote pdb trace).
|
||||||
|
await proc.wait()
|
||||||
else:
|
else:
|
||||||
# `multiprocessing`
|
# `multiprocessing`
|
||||||
assert _ctx
|
assert _ctx
|
||||||
|
@ -235,12 +267,13 @@ async def new_proc(
|
||||||
fs_info = (None, None, None, None, None)
|
fs_info = (None, None, None, None, None)
|
||||||
|
|
||||||
proc = _ctx.Process( # type: ignore
|
proc = _ctx.Process( # type: ignore
|
||||||
target=subactor._mp_main,
|
target=_mp_main,
|
||||||
args=(
|
args=(
|
||||||
|
subactor,
|
||||||
bind_addr,
|
bind_addr,
|
||||||
fs_info,
|
fs_info,
|
||||||
start_method,
|
start_method,
|
||||||
parent_addr
|
parent_addr,
|
||||||
),
|
),
|
||||||
# daemon=True,
|
# daemon=True,
|
||||||
name=name,
|
name=name,
|
||||||
|
|
|
@ -30,7 +30,7 @@ class ActorContextInfo(Mapping):
|
||||||
def __getitem__(self, key: str):
|
def __getitem__(self, key: str):
|
||||||
try:
|
try:
|
||||||
return {
|
return {
|
||||||
'task': trio.hazmat.current_task,
|
'task': trio.lowlevel.current_task,
|
||||||
'actor': current_actor
|
'actor': current_actor
|
||||||
}[key]().name
|
}[key]().name
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
``trio`` inspired apis and helpers
|
``trio`` inspired apis and helpers
|
||||||
"""
|
"""
|
||||||
|
from functools import partial
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
from typing import Tuple, List, Dict, Optional, Any
|
from typing import Tuple, List, Dict, Optional, Any
|
||||||
import typing
|
import typing
|
||||||
|
@ -10,7 +11,7 @@ from async_generator import asynccontextmanager
|
||||||
|
|
||||||
from ._state import current_actor
|
from ._state import current_actor
|
||||||
from .log import get_logger, get_loglevel
|
from .log import get_logger, get_loglevel
|
||||||
from ._actor import Actor # , ActorFailure
|
from ._actor import Actor
|
||||||
from ._portal import Portal
|
from ._portal import Portal
|
||||||
from . import _spawn
|
from . import _spawn
|
||||||
|
|
||||||
|
@ -46,6 +47,7 @@ class ActorNursery:
|
||||||
async def start_actor(
|
async def start_actor(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
*,
|
||||||
bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
|
bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
|
||||||
statespace: Optional[Dict[str, Any]] = None,
|
statespace: Optional[Dict[str, Any]] = None,
|
||||||
rpc_module_paths: List[str] = None,
|
rpc_module_paths: List[str] = None,
|
||||||
|
@ -71,19 +73,22 @@ class ActorNursery:
|
||||||
|
|
||||||
# XXX: the type ignore is actually due to a `mypy` bug
|
# XXX: the type ignore is actually due to a `mypy` bug
|
||||||
return await nursery.start( # type: ignore
|
return await nursery.start( # type: ignore
|
||||||
_spawn.new_proc,
|
partial(
|
||||||
name,
|
_spawn.new_proc,
|
||||||
self,
|
name,
|
||||||
subactor,
|
self,
|
||||||
self.errors,
|
subactor,
|
||||||
bind_addr,
|
self.errors,
|
||||||
parent_addr,
|
bind_addr,
|
||||||
|
parent_addr,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def run_in_actor(
|
async def run_in_actor(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
fn: typing.Callable,
|
fn: typing.Callable,
|
||||||
|
*,
|
||||||
bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
|
bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
|
||||||
rpc_module_paths: Optional[List[str]] = None,
|
rpc_module_paths: Optional[List[str]] = None,
|
||||||
statespace: Dict[str, Any] = None,
|
statespace: Dict[str, Any] = None,
|
||||||
|
@ -131,7 +136,7 @@ class ActorNursery:
|
||||||
# send KeyBoardInterrupt (trio abort signal) to sub-actors
|
# send KeyBoardInterrupt (trio abort signal) to sub-actors
|
||||||
# os.kill(proc.pid, signal.SIGINT)
|
# os.kill(proc.pid, signal.SIGINT)
|
||||||
|
|
||||||
log.debug(f"Cancelling nursery")
|
log.debug("Cancelling nursery")
|
||||||
with trio.move_on_after(3) as cs:
|
with trio.move_on_after(3) as cs:
|
||||||
async with trio.open_nursery() as nursery:
|
async with trio.open_nursery() as nursery:
|
||||||
for subactor, proc, portal in self._children.values():
|
for subactor, proc, portal in self._children.values():
|
||||||
|
@ -260,7 +265,7 @@ async def open_nursery() -> typing.AsyncGenerator[ActorNursery, None]:
|
||||||
|
|
||||||
# Last bit before first nursery block ends in the case
|
# Last bit before first nursery block ends in the case
|
||||||
# where we didn't error in the caller's scope
|
# where we didn't error in the caller's scope
|
||||||
log.debug(f"Waiting on all subactors to complete")
|
log.debug("Waiting on all subactors to complete")
|
||||||
anursery._join_procs.set()
|
anursery._join_procs.set()
|
||||||
|
|
||||||
# ria_nursery scope end
|
# ria_nursery scope end
|
||||||
|
@ -293,4 +298,4 @@ async def open_nursery() -> typing.AsyncGenerator[ActorNursery, None]:
|
||||||
|
|
||||||
# ria_nursery scope end
|
# ria_nursery scope end
|
||||||
|
|
||||||
log.debug(f"Nursery teardown complete")
|
log.debug("Nursery teardown complete")
|
||||||
|
|
|
@ -47,7 +47,7 @@ def tractor_test(fn):
|
||||||
if platform.system() == "Windows":
|
if platform.system() == "Windows":
|
||||||
start_method = 'spawn'
|
start_method = 'spawn'
|
||||||
else:
|
else:
|
||||||
start_method = 'trio_run_in_process'
|
start_method = 'trio'
|
||||||
|
|
||||||
if 'start_method' in inspect.signature(fn).parameters:
|
if 'start_method' in inspect.signature(fn).parameters:
|
||||||
# set of subprocess spawning backends
|
# set of subprocess spawning backends
|
||||||
|
|
Loading…
Reference in New Issue