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
 | 
			
		||||
          language: sh
 | 
			
		||||
          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:
 | 
			
		||||
              - choco install python3 --params "/InstallDir:C:\\Python"
 | 
			
		||||
              - export PATH="/c/Python:/c/Python/Scripts:$PATH"
 | 
			
		||||
| 
						 | 
				
			
			@ -16,6 +27,17 @@ matrix:
 | 
			
		|||
        - name: "Windows, Python 3.7: multiprocessing"
 | 
			
		||||
          os: windows
 | 
			
		||||
          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
 | 
			
		||||
          before_install:
 | 
			
		||||
              - choco install python3 --version 3.7.4 --params "/InstallDir:C:\\Python"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,16 +47,16 @@ matrix:
 | 
			
		|||
        - name: "Python 3.7: multiprocessing"
 | 
			
		||||
          python: 3.7  # this works for Linux but is ignored on macOS or Windows
 | 
			
		||||
          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
 | 
			
		||||
          env: SPAWN_BACKEND="trio_run_in_process"
 | 
			
		||||
          env: SPAWN_BACKEND="trio"
 | 
			
		||||
 | 
			
		||||
        - name: "Python 3.8: multiprocessing"
 | 
			
		||||
          python: 3.8  # this works for Linux but is ignored on macOS or Windows
 | 
			
		||||
          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
 | 
			
		||||
          env: SPAWN_BACKEND="trio_run_in_process"
 | 
			
		||||
          env: SPAWN_BACKEND="trio"
 | 
			
		||||
 | 
			
		||||
install:
 | 
			
		||||
    - cd $TRAVIS_BUILD_DIR
 | 
			
		||||
| 
						 | 
				
			
			@ -43,4 +65,4 @@ install:
 | 
			
		|||
 | 
			
		||||
script:
 | 
			
		||||
    - 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=[
 | 
			
		||||
        'msgpack', 'trio>0.8', 'async_generator', 'colorlog', 'wrapt',
 | 
			
		||||
        'trio_typing', 'trio-run-in-process',
 | 
			
		||||
        'trio_typing', 'cloudpickle',
 | 
			
		||||
    ],
 | 
			
		||||
    tests_require=['pytest'],
 | 
			
		||||
    python_requires=">=3.7",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,27 @@
 | 
			
		|||
"""
 | 
			
		||||
``tractor`` testing!!
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import platform
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
import tractor
 | 
			
		||||
from tractor.testing import tractor_test
 | 
			
		||||
 | 
			
		||||
# export for tests
 | 
			
		||||
from tractor.testing import tractor_test  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
pytest_plugins = ['pytester']
 | 
			
		||||
_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):
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--ll", action="store", dest='loglevel',
 | 
			
		||||
| 
						 | 
				
			
			@ -21,7 +30,7 @@ def pytest_addoption(parser):
 | 
			
		|||
 | 
			
		||||
    parser.addoption(
 | 
			
		||||
        "--spawn-backend", action="store", dest='spawn_backend',
 | 
			
		||||
        default='trio_run_in_process',
 | 
			
		||||
        default='trio',
 | 
			
		||||
        help="Processing spawning backend to use for test run",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,12 +38,9 @@ def pytest_addoption(parser):
 | 
			
		|||
def pytest_configure(config):
 | 
			
		||||
    backend = config.option.spawn_backend
 | 
			
		||||
 | 
			
		||||
    if platform.system() == "Windows":
 | 
			
		||||
        backend = 'mp'
 | 
			
		||||
 | 
			
		||||
    if backend == 'mp':
 | 
			
		||||
        tractor._spawn.try_set_start_method('spawn')
 | 
			
		||||
    elif backend == 'trio_run_in_process':
 | 
			
		||||
    elif backend == 'trio':
 | 
			
		||||
        tractor._spawn.try_set_start_method(backend)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +52,18 @@ def loglevel(request):
 | 
			
		|||
    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')
 | 
			
		||||
def arb_addr():
 | 
			
		||||
    return _arb_addr
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +74,7 @@ def pytest_generate_tests(metafunc):
 | 
			
		|||
    if not spawn_backend:
 | 
			
		||||
        # XXX some weird windows bug with `pytest`?
 | 
			
		||||
        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 spawn_backend == 'mp':
 | 
			
		||||
| 
						 | 
				
			
			@ -67,11 +85,7 @@ def pytest_generate_tests(metafunc):
 | 
			
		|||
                # removing XXX: the fork method is in general
 | 
			
		||||
                # incompatible with trio's global scheduler state
 | 
			
		||||
                methods.remove('fork')
 | 
			
		||||
        elif spawn_backend == 'trio_run_in_process':
 | 
			
		||||
            if platform.system() == "Windows":
 | 
			
		||||
                pytest.fail(
 | 
			
		||||
                    "Only `--spawn-backend=mp` is supported on Windows")
 | 
			
		||||
 | 
			
		||||
            methods = ['trio_run_in_process']
 | 
			
		||||
        elif spawn_backend == 'trio':
 | 
			
		||||
            methods = ['trio']
 | 
			
		||||
 | 
			
		||||
        metafunc.parametrize("start_method", methods, scope='module')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
"""
 | 
			
		||||
Cancellation and error propagation
 | 
			
		||||
"""
 | 
			
		||||
import os
 | 
			
		||||
import signal
 | 
			
		||||
import platform
 | 
			
		||||
from itertools import repeat
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -8,7 +10,7 @@ import pytest
 | 
			
		|||
import trio
 | 
			
		||||
import tractor
 | 
			
		||||
 | 
			
		||||
from conftest import tractor_test
 | 
			
		||||
from conftest import tractor_test, no_windows
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def assert_err(delay=0):
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +19,7 @@ async def assert_err(delay=0):
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
async def sleep_forever():
 | 
			
		||||
    await trio.sleep(float('inf'))
 | 
			
		||||
    await trio.sleep_forever()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def do_nuthin():
 | 
			
		||||
| 
						 | 
				
			
			@ -118,7 +120,8 @@ def do_nothing():
 | 
			
		|||
    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
 | 
			
		||||
    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
 | 
			
		||||
 | 
			
		||||
            # would hang otherwise
 | 
			
		||||
            await nursery.cancel()
 | 
			
		||||
            if mechanism == '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():
 | 
			
		||||
| 
						 | 
				
			
			@ -153,7 +163,7 @@ async def test_cancel_infinite_streamer(start_method):
 | 
			
		|||
    with trio.move_on_after(1) as cancel_scope:
 | 
			
		||||
        async with tractor.open_nursery() as n:
 | 
			
		||||
            portal = await n.start_actor(
 | 
			
		||||
                f'donny',
 | 
			
		||||
                'donny',
 | 
			
		||||
                rpc_module_paths=[__name__],
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -197,7 +207,7 @@ async def test_cancel_infinite_streamer(start_method):
 | 
			
		|||
    ],
 | 
			
		||||
)
 | 
			
		||||
@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
 | 
			
		||||
    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
 | 
			
		||||
    for arbitrary n-depth actor trees.
 | 
			
		||||
    """
 | 
			
		||||
    if start_method == 'trio_run_in_process':
 | 
			
		||||
    if start_method == 'trio':
 | 
			
		||||
        depth = 3
 | 
			
		||||
        subactor_breadth = 2
 | 
			
		||||
    else:
 | 
			
		||||
| 
						 | 
				
			
			@ -299,7 +309,7 @@ async def test_nested_multierrors(loglevel, start_method):
 | 
			
		|||
        # hangs and broken pipes all over the place...
 | 
			
		||||
        if start_method == 'forkserver':
 | 
			
		||||
            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
 | 
			
		||||
 | 
			
		||||
    with trio.fail_after(120):
 | 
			
		||||
| 
						 | 
				
			
			@ -315,10 +325,29 @@ async def test_nested_multierrors(loglevel, start_method):
 | 
			
		|||
        except trio.MultiError as err:
 | 
			
		||||
            assert len(err.exceptions) == subactor_breadth
 | 
			
		||||
            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..
 | 
			
		||||
                    # on windows sometimes spawning is just too slow and
 | 
			
		||||
                    # we get back the (sent) cancel signal instead
 | 
			
		||||
                    if platform.system() == 'Windows':
 | 
			
		||||
                        assert (subexc.type is trio.MultiError) or (
 | 
			
		||||
                            subexc.type is tractor.RemoteActorError)
 | 
			
		||||
| 
						 | 
				
			
			@ -327,3 +356,50 @@ async def test_nested_multierrors(loglevel, start_method):
 | 
			
		|||
                else:
 | 
			
		||||
                    assert (subexc.type is tractor.RemoteActorError) or (
 | 
			
		||||
                        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')
 | 
			
		||||
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()
 | 
			
		||||
    results = tractor.run(cancel_after, timeout, arbiter_addr=arb_addr)
 | 
			
		||||
    diff = time.time() - start
 | 
			
		||||
| 
						 | 
				
			
			@ -211,8 +211,12 @@ def time_quad_ex(arb_addr):
 | 
			
		|||
    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"."""
 | 
			
		||||
    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
 | 
			
		||||
    assert results
 | 
			
		||||
    this_fast = 6 if platform.system() == 'Windows' else 2.5
 | 
			
		||||
| 
						 | 
				
			
			@ -223,10 +227,16 @@ def test_a_quadruple_example(time_quad_ex):
 | 
			
		|||
    'cancel_delay',
 | 
			
		||||
    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
 | 
			
		||||
    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
 | 
			
		||||
    delay = max(diff - cancel_delay, 0)
 | 
			
		||||
    results = tractor.run(cancel_after, delay, arbiter_addr=arb_addr)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,7 +20,7 @@ from async_generator import aclosing
 | 
			
		|||
 | 
			
		||||
from ._ipc import Channel
 | 
			
		||||
from ._streaming import Context, _context
 | 
			
		||||
from .log import get_console_log, get_logger
 | 
			
		||||
from .log import get_logger
 | 
			
		||||
from ._exceptions import (
 | 
			
		||||
    pack_error,
 | 
			
		||||
    unpack_error,
 | 
			
		||||
| 
						 | 
				
			
			@ -149,7 +149,7 @@ async def _invoke(
 | 
			
		|||
                f"Task {func} was likely cancelled before it was started")
 | 
			
		||||
 | 
			
		||||
        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()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -256,7 +256,7 @@ class Actor:
 | 
			
		|||
        code (if it exists).
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            if self._spawn_method == 'trio_run_in_process':
 | 
			
		||||
            if self._spawn_method == 'trio':
 | 
			
		||||
                parent_data = self._parent_main_data
 | 
			
		||||
                if 'init_main_from_name' in parent_data:
 | 
			
		||||
                    _mp_fixup_main._fixup_main_from_name(
 | 
			
		||||
| 
						 | 
				
			
			@ -339,7 +339,7 @@ class Actor:
 | 
			
		|||
 | 
			
		||||
            if not self._peers:  # no more channels connected
 | 
			
		||||
                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?)
 | 
			
		||||
            if chan.connected():
 | 
			
		||||
| 
						 | 
				
			
			@ -539,58 +539,6 @@ class Actor:
 | 
			
		|||
                f"Exiting msg loop for {chan} from {chan.uid} "
 | 
			
		||||
                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(
 | 
			
		||||
        self,
 | 
			
		||||
        accept_addr: Tuple[str, int],
 | 
			
		||||
| 
						 | 
				
			
			@ -661,9 +609,18 @@ class Actor:
 | 
			
		|||
            # killed (i.e. this actor is cancelled or signalled by the parent)
 | 
			
		||||
        except Exception as err:
 | 
			
		||||
            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(
 | 
			
		||||
                    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:
 | 
			
		||||
                try:
 | 
			
		||||
| 
						 | 
				
			
			@ -681,6 +638,7 @@ class Actor:
 | 
			
		|||
                # XXX wait, why?
 | 
			
		||||
                # causes a hang if I always raise..
 | 
			
		||||
                # A parent process does something weird here?
 | 
			
		||||
                # i'm so lost now..
 | 
			
		||||
                raise
 | 
			
		||||
 | 
			
		||||
        finally:
 | 
			
		||||
| 
						 | 
				
			
			@ -695,7 +653,7 @@ class Actor:
 | 
			
		|||
                    log.debug(
 | 
			
		||||
                        f"Waiting for remaining peers {self._peers} to clear")
 | 
			
		||||
                    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
 | 
			
		||||
            # or completed
 | 
			
		||||
| 
						 | 
				
			
			@ -729,8 +687,8 @@ class Actor:
 | 
			
		|||
                    port=accept_port, host=accept_host,
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            log.debug(f"Started tcp server(s) on"
 | 
			
		||||
                      " {[l.socket for l in listeners]}")  # type: ignore
 | 
			
		||||
            log.debug("Started tcp server(s) on"  # type: ignore
 | 
			
		||||
                      f" {[l.socket for l in listeners]}")
 | 
			
		||||
            self._listeners.extend(listeners)
 | 
			
		||||
            task_status.started()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -917,7 +875,7 @@ async def _start_actor(
 | 
			
		|||
    port: int,
 | 
			
		||||
    arbiter_addr: Tuple[str, int],
 | 
			
		||||
    nursery: trio.Nursery = None
 | 
			
		||||
):
 | 
			
		||||
) -> Any:
 | 
			
		||||
    """Spawn a local actor by starting a task to execute it's main async
 | 
			
		||||
    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.
 | 
			
		||||
"""
 | 
			
		||||
import sys
 | 
			
		||||
import inspect
 | 
			
		||||
import subprocess
 | 
			
		||||
import multiprocessing as mp
 | 
			
		||||
import platform
 | 
			
		||||
from typing import Any, Dict, Optional
 | 
			
		||||
from functools import partial
 | 
			
		||||
 | 
			
		||||
import trio
 | 
			
		||||
import cloudpickle
 | 
			
		||||
from trio_typing import TaskStatus
 | 
			
		||||
from async_generator import aclosing
 | 
			
		||||
from async_generator import aclosing, asynccontextmanager
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    from multiprocessing import semaphore_tracker  # type: ignore
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +30,7 @@ from ._state import current_actor
 | 
			
		|||
from .log import get_logger
 | 
			
		||||
from ._portal import Portal
 | 
			
		||||
from ._actor import Actor, ActorFailure
 | 
			
		||||
from ._entry import _mp_main, _trio_main
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = get_logger('tractor')
 | 
			
		||||
| 
						 | 
				
			
			@ -40,23 +45,23 @@ if platform.system() == 'Windows':
 | 
			
		|||
    _ctx = mp.get_context("spawn")
 | 
			
		||||
 | 
			
		||||
    async def proc_waiter(proc: mp.Process) -> None:
 | 
			
		||||
        await trio.hazmat.WaitForSingleObject(proc.sentinel)
 | 
			
		||||
        await trio.lowlevel.WaitForSingleObject(proc.sentinel)
 | 
			
		||||
else:
 | 
			
		||||
    # *NIX systems use ``trio_run_in_process` as our default (for now)
 | 
			
		||||
    import trio_run_in_process
 | 
			
		||||
    _spawn_method = "trio_run_in_process"
 | 
			
		||||
    # *NIX systems use ``trio`` primitives as our default
 | 
			
		||||
    _spawn_method = "trio"
 | 
			
		||||
 | 
			
		||||
    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]:
 | 
			
		||||
    """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".
 | 
			
		||||
 | 
			
		||||
    If the desired method is not supported this function will error. On
 | 
			
		||||
    Windows the only supported option is the ``multiprocessing`` "spawn"
 | 
			
		||||
    method. The default on *nix systems is ``trio_run_in_process``.
 | 
			
		||||
    If the desired method is not supported this function will error.
 | 
			
		||||
    On Windows only the ``multiprocessing`` "spawn" method is offered
 | 
			
		||||
    besides the default ``trio`` which uses async wrapping around
 | 
			
		||||
    ``subprocess.Popen``.
 | 
			
		||||
    """
 | 
			
		||||
    global _ctx
 | 
			
		||||
    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
 | 
			
		||||
        methods.remove('fork')
 | 
			
		||||
 | 
			
		||||
    # no Windows support for trip yet
 | 
			
		||||
    if platform.system() != 'Windows':
 | 
			
		||||
        methods += ['trio_run_in_process']
 | 
			
		||||
    # supported on all platforms
 | 
			
		||||
    methods += ['trio']
 | 
			
		||||
 | 
			
		||||
    if name not in methods:
 | 
			
		||||
        raise ValueError(
 | 
			
		||||
| 
						 | 
				
			
			@ -77,7 +81,7 @@ def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
 | 
			
		|||
    elif name == 'forkserver':
 | 
			
		||||
        _forkserver_override.override_stdlib()
 | 
			
		||||
        _ctx = mp.get_context(name)
 | 
			
		||||
    elif name == 'trio_run_in_process':
 | 
			
		||||
    elif name == 'trio':
 | 
			
		||||
        _ctx = None
 | 
			
		||||
    else:
 | 
			
		||||
        _ctx = mp.get_context(name)
 | 
			
		||||
| 
						 | 
				
			
			@ -118,6 +122,7 @@ async def exhaust_portal(
 | 
			
		|||
        # we reraise in the parent task via a ``trio.MultiError``
 | 
			
		||||
        return err
 | 
			
		||||
    else:
 | 
			
		||||
        log.debug(f"Returning final result: {final}")
 | 
			
		||||
        return final
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -152,6 +157,29 @@ async def cancel_on_completion(
 | 
			
		|||
        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(
 | 
			
		||||
    name: str,
 | 
			
		||||
    actor_nursery: 'ActorNursery',  # type: ignore
 | 
			
		||||
| 
						 | 
				
			
			@ -172,10 +200,11 @@ async def new_proc(
 | 
			
		|||
    subactor._spawn_method = _spawn_method
 | 
			
		||||
 | 
			
		||||
    async with trio.open_nursery() as nursery:
 | 
			
		||||
        if use_trio_run_in_process or _spawn_method == 'trio_run_in_process':
 | 
			
		||||
            # trio_run_in_process
 | 
			
		||||
            async with trio_run_in_process.open_in_process(
 | 
			
		||||
                subactor._trip_main,
 | 
			
		||||
        if use_trio_run_in_process or _spawn_method == 'trio':
 | 
			
		||||
            async with run_in_process(
 | 
			
		||||
                subactor,
 | 
			
		||||
                _trio_main,
 | 
			
		||||
                subactor,
 | 
			
		||||
                bind_addr,
 | 
			
		||||
                parent_addr,
 | 
			
		||||
            ) as proc:
 | 
			
		||||
| 
						 | 
				
			
			@ -198,7 +227,10 @@ async def new_proc(
 | 
			
		|||
                    cancel_scope = await nursery.start(
 | 
			
		||||
                        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:
 | 
			
		||||
            # `multiprocessing`
 | 
			
		||||
            assert _ctx
 | 
			
		||||
| 
						 | 
				
			
			@ -235,12 +267,13 @@ async def new_proc(
 | 
			
		|||
                fs_info = (None, None, None, None, None)
 | 
			
		||||
 | 
			
		||||
            proc = _ctx.Process(  # type: ignore
 | 
			
		||||
                target=subactor._mp_main,
 | 
			
		||||
                target=_mp_main,
 | 
			
		||||
                args=(
 | 
			
		||||
                    subactor,
 | 
			
		||||
                    bind_addr,
 | 
			
		||||
                    fs_info,
 | 
			
		||||
                    start_method,
 | 
			
		||||
                    parent_addr
 | 
			
		||||
                    parent_addr,
 | 
			
		||||
                ),
 | 
			
		||||
                # daemon=True,
 | 
			
		||||
                name=name,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,7 +30,7 @@ class ActorContextInfo(Mapping):
 | 
			
		|||
    def __getitem__(self, key: str):
 | 
			
		||||
        try:
 | 
			
		||||
            return {
 | 
			
		||||
                'task': trio.hazmat.current_task,
 | 
			
		||||
                'task': trio.lowlevel.current_task,
 | 
			
		||||
                'actor': current_actor
 | 
			
		||||
            }[key]().name
 | 
			
		||||
        except RuntimeError:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,7 @@
 | 
			
		|||
"""
 | 
			
		||||
``trio`` inspired apis and helpers
 | 
			
		||||
"""
 | 
			
		||||
from functools import partial
 | 
			
		||||
import multiprocessing as mp
 | 
			
		||||
from typing import Tuple, List, Dict, Optional, Any
 | 
			
		||||
import typing
 | 
			
		||||
| 
						 | 
				
			
			@ -10,7 +11,7 @@ from async_generator import asynccontextmanager
 | 
			
		|||
 | 
			
		||||
from ._state import current_actor
 | 
			
		||||
from .log import get_logger, get_loglevel
 | 
			
		||||
from ._actor import Actor  # , ActorFailure
 | 
			
		||||
from ._actor import Actor
 | 
			
		||||
from ._portal import Portal
 | 
			
		||||
from . import _spawn
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,6 +47,7 @@ class ActorNursery:
 | 
			
		|||
    async def start_actor(
 | 
			
		||||
        self,
 | 
			
		||||
        name: str,
 | 
			
		||||
        *,
 | 
			
		||||
        bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
 | 
			
		||||
        statespace: Optional[Dict[str, Any]] = None,
 | 
			
		||||
        rpc_module_paths: List[str] = None,
 | 
			
		||||
| 
						 | 
				
			
			@ -71,19 +73,22 @@ class ActorNursery:
 | 
			
		|||
 | 
			
		||||
        # XXX: the type ignore is actually due to a `mypy` bug
 | 
			
		||||
        return await nursery.start(  # type: ignore
 | 
			
		||||
            _spawn.new_proc,
 | 
			
		||||
            name,
 | 
			
		||||
            self,
 | 
			
		||||
            subactor,
 | 
			
		||||
            self.errors,
 | 
			
		||||
            bind_addr,
 | 
			
		||||
            parent_addr,
 | 
			
		||||
            partial(
 | 
			
		||||
                _spawn.new_proc,
 | 
			
		||||
                name,
 | 
			
		||||
                self,
 | 
			
		||||
                subactor,
 | 
			
		||||
                self.errors,
 | 
			
		||||
                bind_addr,
 | 
			
		||||
                parent_addr,
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    async def run_in_actor(
 | 
			
		||||
        self,
 | 
			
		||||
        name: str,
 | 
			
		||||
        fn: typing.Callable,
 | 
			
		||||
        *,
 | 
			
		||||
        bind_addr: Tuple[str, int] = ('127.0.0.1', 0),
 | 
			
		||||
        rpc_module_paths: Optional[List[str]] = None,
 | 
			
		||||
        statespace: Dict[str, Any] = None,
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +136,7 @@ class ActorNursery:
 | 
			
		|||
            # send KeyBoardInterrupt (trio abort signal) to sub-actors
 | 
			
		||||
            # os.kill(proc.pid, signal.SIGINT)
 | 
			
		||||
 | 
			
		||||
        log.debug(f"Cancelling nursery")
 | 
			
		||||
        log.debug("Cancelling nursery")
 | 
			
		||||
        with trio.move_on_after(3) as cs:
 | 
			
		||||
            async with trio.open_nursery() as nursery:
 | 
			
		||||
                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
 | 
			
		||||
                # 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()
 | 
			
		||||
 | 
			
		||||
                # ria_nursery scope end
 | 
			
		||||
| 
						 | 
				
			
			@ -293,4 +298,4 @@ async def open_nursery() -> typing.AsyncGenerator[ActorNursery, None]:
 | 
			
		|||
 | 
			
		||||
        # 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":
 | 
			
		||||
                start_method = 'spawn'
 | 
			
		||||
            else:
 | 
			
		||||
                start_method = 'trio_run_in_process'
 | 
			
		||||
                start_method = 'trio'
 | 
			
		||||
 | 
			
		||||
        if 'start_method' in inspect.signature(fn).parameters:
 | 
			
		||||
            # set of subprocess spawning backends
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue