2018-06-07 04:29:17 +00:00
|
|
|
"""
|
2026-04-10 16:03:59 +00:00
|
|
|
Spawning basics including audit of,
|
|
|
|
|
|
2026-04-10 18:55:09 +00:00
|
|
|
- subproc bootstrap, such as subactor runtime-data/config inheritance,
|
2026-04-10 16:03:59 +00:00
|
|
|
- basic (and mostly legacy) `ActorNursery` subactor starting and
|
|
|
|
|
cancel APIs.
|
|
|
|
|
|
|
|
|
|
Simple (and generally legacy) examples from the original
|
|
|
|
|
API design.
|
2022-08-29 19:08:04 +00:00
|
|
|
|
2018-06-07 04:29:17 +00:00
|
|
|
"""
|
2025-04-03 20:15:53 +00:00
|
|
|
from functools import partial
|
2024-05-09 20:31:23 +00:00
|
|
|
from typing import (
|
|
|
|
|
Any,
|
|
|
|
|
)
|
2020-12-21 14:09:55 +00:00
|
|
|
|
2019-03-24 00:29:37 +00:00
|
|
|
import pytest
|
2018-06-12 19:23:58 +00:00
|
|
|
import trio
|
2018-07-05 23:49:21 +00:00
|
|
|
import tractor
|
2018-06-07 04:29:17 +00:00
|
|
|
|
2024-03-12 19:48:20 +00:00
|
|
|
from tractor._testing import tractor_test
|
2018-07-10 21:19:54 +00:00
|
|
|
|
2025-04-03 20:15:53 +00:00
|
|
|
data_to_pass_down = {
|
|
|
|
|
'doggy': 10,
|
|
|
|
|
'kitty': 4,
|
|
|
|
|
}
|
2018-07-11 23:24:37 +00:00
|
|
|
|
|
|
|
|
|
2021-09-06 16:07:09 +00:00
|
|
|
async def spawn(
|
2025-04-03 20:15:53 +00:00
|
|
|
should_be_root: bool,
|
2022-09-15 20:56:50 +00:00
|
|
|
data: dict,
|
2025-03-20 21:50:22 +00:00
|
|
|
reg_addr: tuple[str, int],
|
2018-06-19 15:49:25 +00:00
|
|
|
|
2025-04-03 20:15:53 +00:00
|
|
|
debug_mode: bool = False,
|
|
|
|
|
):
|
2018-06-19 15:49:25 +00:00
|
|
|
await trio.sleep(0.1)
|
2025-04-03 20:15:53 +00:00
|
|
|
actor = tractor.current_actor(err_on_no_runtime=False)
|
|
|
|
|
|
|
|
|
|
if should_be_root:
|
|
|
|
|
assert actor is None # no runtime yet
|
|
|
|
|
async with (
|
|
|
|
|
tractor.open_root_actor(
|
2026-04-13 16:11:54 +00:00
|
|
|
registry_addrs=[reg_addr],
|
2025-04-03 20:15:53 +00:00
|
|
|
),
|
|
|
|
|
tractor.open_nursery() as an,
|
|
|
|
|
):
|
|
|
|
|
# now runtime exists
|
|
|
|
|
actor: tractor.Actor = tractor.current_actor()
|
Rename `Arbiter` -> `Registrar`, mv to `discovery._registry`
Move the `Arbiter` class out of `runtime._runtime` into its
logical home at `discovery._registry` as `Registrar(Actor)`.
This completes the long-standing terminology migration from
"arbiter" to "registrar/registry" throughout the codebase.
Deats,
- add new `discovery/_registry.py` mod with `Registrar`
class + backward-compat `Arbiter = Registrar` alias.
- rename `Actor.is_arbiter` attr -> `.is_registrar`;
old attr now a `@property` with `DeprecationWarning`.
- `_root.py` imports `Registrar` directly for
root-actor instantiation.
- export `Registrar` + `Arbiter` from `tractor.__init__`.
- `_runtime.py` re-imports from `discovery._registry`
for backward compat.
Also,
- update all test files to use `.is_registrar`
(`test_local`, `test_rpc`, `test_spawning`,
`test_discovery`, `test_multi_program`).
- update "arbiter" -> "registrar" in comments/docstrings
across `_discovery.py`, `_server.py`, `_transport.py`,
`_testing/pytest.py`, and examples.
- drop resolved TODOs from `_runtime.py` and `_root.py`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-23 22:56:21 +00:00
|
|
|
assert actor.is_registrar == should_be_root
|
2025-04-03 20:15:53 +00:00
|
|
|
|
|
|
|
|
# spawns subproc here
|
|
|
|
|
portal: tractor.Portal = await an.run_in_actor(
|
|
|
|
|
fn=spawn,
|
|
|
|
|
|
|
|
|
|
# spawning args
|
|
|
|
|
name='sub-actor',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
|
|
|
|
|
# passed to a subactor-recursive RPC invoke
|
|
|
|
|
# of this same `spawn()` fn.
|
|
|
|
|
should_be_root=False,
|
|
|
|
|
data=data_to_pass_down,
|
|
|
|
|
reg_addr=reg_addr,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert len(an._children) == 1
|
2025-04-11 20:55:03 +00:00
|
|
|
assert (
|
|
|
|
|
portal.channel.uid
|
|
|
|
|
in
|
|
|
|
|
tractor.current_actor().ipc_server._peers
|
|
|
|
|
)
|
2018-08-02 19:27:09 +00:00
|
|
|
|
2025-04-03 20:15:53 +00:00
|
|
|
# get result from child subactor
|
|
|
|
|
result = await portal.result()
|
|
|
|
|
assert result == 10
|
|
|
|
|
return result
|
|
|
|
|
else:
|
Rename `Arbiter` -> `Registrar`, mv to `discovery._registry`
Move the `Arbiter` class out of `runtime._runtime` into its
logical home at `discovery._registry` as `Registrar(Actor)`.
This completes the long-standing terminology migration from
"arbiter" to "registrar/registry" throughout the codebase.
Deats,
- add new `discovery/_registry.py` mod with `Registrar`
class + backward-compat `Arbiter = Registrar` alias.
- rename `Actor.is_arbiter` attr -> `.is_registrar`;
old attr now a `@property` with `DeprecationWarning`.
- `_root.py` imports `Registrar` directly for
root-actor instantiation.
- export `Registrar` + `Arbiter` from `tractor.__init__`.
- `_runtime.py` re-imports from `discovery._registry`
for backward compat.
Also,
- update all test files to use `.is_registrar`
(`test_local`, `test_rpc`, `test_spawning`,
`test_discovery`, `test_multi_program`).
- update "arbiter" -> "registrar" in comments/docstrings
across `_discovery.py`, `_server.py`, `_transport.py`,
`_testing/pytest.py`, and examples.
- drop resolved TODOs from `_runtime.py` and `_root.py`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-23 22:56:21 +00:00
|
|
|
assert actor.is_registrar == should_be_root
|
2025-04-03 20:15:53 +00:00
|
|
|
return 10
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_run_in_actor_same_func_in_child(
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
debug_mode: bool,
|
2023-10-19 15:17:07 +00:00
|
|
|
):
|
2021-02-24 23:16:10 +00:00
|
|
|
result = trio.run(
|
2025-04-03 20:15:53 +00:00
|
|
|
partial(
|
|
|
|
|
spawn,
|
|
|
|
|
should_be_root=True,
|
|
|
|
|
data=data_to_pass_down,
|
|
|
|
|
reg_addr=reg_addr,
|
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
|
)
|
2018-06-19 15:49:25 +00:00
|
|
|
)
|
2018-07-06 06:45:26 +00:00
|
|
|
assert result == 10
|
|
|
|
|
|
2018-06-12 19:23:58 +00:00
|
|
|
|
2021-04-27 13:14:08 +00:00
|
|
|
async def movie_theatre_question():
|
2025-04-03 20:15:53 +00:00
|
|
|
'''
|
|
|
|
|
A question asked in a dark theatre, in a tangent
|
2018-07-10 21:19:54 +00:00
|
|
|
(errr, I mean different) process.
|
2025-04-03 20:15:53 +00:00
|
|
|
|
|
|
|
|
'''
|
2018-07-10 21:19:54 +00:00
|
|
|
return 'have you ever seen a portal?'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@tractor_test
|
2026-04-10 16:03:59 +00:00
|
|
|
async def test_movie_theatre_convo(
|
|
|
|
|
start_method: str,
|
|
|
|
|
):
|
2025-04-03 20:15:53 +00:00
|
|
|
'''
|
|
|
|
|
The main ``tractor`` routine.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
async with tractor.open_nursery(debug_mode=True) as an:
|
2018-08-03 04:55:50 +00:00
|
|
|
|
2025-04-03 20:15:53 +00:00
|
|
|
portal = await an.start_actor(
|
2018-07-10 21:19:54 +00:00
|
|
|
'frank',
|
|
|
|
|
# enable the actor to run funcs from this current module
|
2021-02-24 23:16:10 +00:00
|
|
|
enable_modules=[__name__],
|
2018-07-10 21:19:54 +00:00
|
|
|
)
|
|
|
|
|
|
2020-12-22 15:35:05 +00:00
|
|
|
print(await portal.run(movie_theatre_question))
|
2018-08-03 04:55:50 +00:00
|
|
|
# call the subactor a 2nd time
|
2020-12-22 15:35:05 +00:00
|
|
|
print(await portal.run(movie_theatre_question))
|
2018-07-10 21:19:54 +00:00
|
|
|
|
|
|
|
|
# the async with will block here indefinitely waiting
|
2018-08-02 19:27:09 +00:00
|
|
|
# for our actor "frank" to complete, we cancel 'frank'
|
|
|
|
|
# to avoid blocking indefinitely
|
2018-07-10 21:19:54 +00:00
|
|
|
await portal.cancel_actor()
|
|
|
|
|
|
|
|
|
|
|
2024-05-09 20:31:23 +00:00
|
|
|
async def cellar_door(
|
|
|
|
|
return_value: str|None,
|
|
|
|
|
):
|
2021-11-20 17:51:29 +00:00
|
|
|
return return_value
|
2018-07-10 21:19:54 +00:00
|
|
|
|
|
|
|
|
|
2021-11-20 17:51:29 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
'return_value', ["Dang that's beautiful", None],
|
|
|
|
|
ids=['return_str', 'return_None'],
|
|
|
|
|
)
|
2018-07-10 21:19:54 +00:00
|
|
|
@tractor_test
|
2021-11-20 17:51:29 +00:00
|
|
|
async def test_most_beautiful_word(
|
2024-05-09 20:31:23 +00:00
|
|
|
start_method: str,
|
|
|
|
|
return_value: Any,
|
|
|
|
|
debug_mode: bool,
|
2021-11-20 17:51:29 +00:00
|
|
|
):
|
|
|
|
|
'''
|
|
|
|
|
The main ``tractor`` routine.
|
2018-08-03 04:55:50 +00:00
|
|
|
|
2021-11-20 17:51:29 +00:00
|
|
|
'''
|
2021-11-20 18:08:19 +00:00
|
|
|
with trio.fail_after(1):
|
2024-05-09 20:31:23 +00:00
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
debug_mode=debug_mode,
|
2025-04-03 20:15:53 +00:00
|
|
|
) as an:
|
|
|
|
|
portal = await an.run_in_actor(
|
2021-11-20 17:51:29 +00:00
|
|
|
cellar_door,
|
|
|
|
|
return_value=return_value,
|
|
|
|
|
name='some_linguist',
|
|
|
|
|
)
|
2018-07-10 21:19:54 +00:00
|
|
|
|
2026-04-10 18:55:09 +00:00
|
|
|
res: Any = await portal.wait_for_result()
|
|
|
|
|
assert res == return_value
|
2018-07-10 21:19:54 +00:00
|
|
|
# The ``async with`` will unblock here since the 'some_linguist'
|
|
|
|
|
# actor has completed its main task ``cellar_door``.
|
|
|
|
|
|
2021-11-20 17:51:29 +00:00
|
|
|
# this should pull the cached final result already captured during
|
|
|
|
|
# the nursery block exit.
|
2026-04-10 16:03:59 +00:00
|
|
|
res: Any = await portal.wait_for_result()
|
|
|
|
|
assert res == return_value
|
|
|
|
|
print(res)
|
2019-03-24 00:29:37 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def check_loglevel(level):
|
|
|
|
|
assert tractor.current_actor().loglevel == level
|
|
|
|
|
log = tractor.log.get_logger()
|
|
|
|
|
# XXX using a level actually used inside tractor seems to trigger
|
|
|
|
|
# some kind of `logging` module bug FYI.
|
|
|
|
|
log.critical('yoyoyo')
|
|
|
|
|
|
|
|
|
|
|
2026-04-10 16:03:59 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
'level', [
|
|
|
|
|
'debug',
|
|
|
|
|
'cancel',
|
|
|
|
|
'critical'
|
|
|
|
|
],
|
|
|
|
|
ids='loglevel={}'.format,
|
|
|
|
|
)
|
|
|
|
|
def test_loglevel_propagated_to_subactor(
|
|
|
|
|
capfd: pytest.CaptureFixture,
|
|
|
|
|
start_method: str,
|
|
|
|
|
reg_addr: tuple,
|
|
|
|
|
level: str,
|
|
|
|
|
):
|
|
|
|
|
if start_method == 'mp_forkserver':
|
|
|
|
|
pytest.skip(
|
|
|
|
|
"a bug with `capfd` seems to make forkserver capture not work?"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
async with tractor.open_nursery(
|
|
|
|
|
name='registrar',
|
|
|
|
|
start_method=start_method,
|
2026-04-13 16:11:54 +00:00
|
|
|
registry_addrs=[reg_addr],
|
2026-04-10 16:03:59 +00:00
|
|
|
|
|
|
|
|
) as tn:
|
|
|
|
|
await tn.run_in_actor(
|
|
|
|
|
check_loglevel,
|
|
|
|
|
loglevel=level,
|
|
|
|
|
level=level,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
|
|
|
|
|
|
|
|
|
# ensure subactor spits log message on stderr
|
|
|
|
|
captured = capfd.readouterr()
|
|
|
|
|
assert 'yoyoyo' in captured.err
|
|
|
|
|
|
|
|
|
|
|
2026-04-09 23:14:09 +00:00
|
|
|
async def check_parent_main_inheritance(
|
|
|
|
|
expect_inherited: bool,
|
|
|
|
|
) -> bool:
|
|
|
|
|
'''
|
|
|
|
|
Assert that the child actor's ``_parent_main_data`` matches the
|
|
|
|
|
``inherit_parent_main`` flag it was spawned with.
|
|
|
|
|
|
|
|
|
|
With the trio spawn backend the parent's ``__main__`` bootstrap
|
|
|
|
|
data is captured and forwarded to each child so it can replay
|
|
|
|
|
the parent's ``__main__`` as ``__mp_main__``, mirroring the
|
|
|
|
|
stdlib ``multiprocessing`` bootstrap:
|
|
|
|
|
https://docs.python.org/3/library/multiprocessing.html#the-spawn-and-forkserver-start-methods
|
|
|
|
|
|
|
|
|
|
When ``inherit_parent_main=False`` the data dict is empty
|
|
|
|
|
(``{}``) so no fixup ever runs and the child keeps its own
|
|
|
|
|
``__main__`` untouched.
|
|
|
|
|
|
2026-04-10 16:03:59 +00:00
|
|
|
NOTE: under `pytest` the parent ``__main__`` is
|
|
|
|
|
``pytest.__main__`` whose ``_fixup_main_from_name()`` is a no-op
|
|
|
|
|
(the name ends with ``.__main__``), so we cannot observe
|
|
|
|
|
a difference in ``sys.modules['__main__'].__name__`` between the
|
|
|
|
|
two modes. Checking ``_parent_main_data`` directly is the most
|
|
|
|
|
reliable verification that the flag is threaded through
|
|
|
|
|
correctly; a ``RemoteActorError[AssertionError]`` propagates on
|
|
|
|
|
mismatch.
|
|
|
|
|
|
2026-04-09 23:14:09 +00:00
|
|
|
'''
|
|
|
|
|
import tractor
|
2026-04-10 16:03:59 +00:00
|
|
|
actor: tractor.Actor = tractor.current_actor()
|
2026-04-09 23:14:09 +00:00
|
|
|
has_data: bool = bool(actor._parent_main_data)
|
|
|
|
|
assert has_data == expect_inherited, (
|
|
|
|
|
f'Expected _parent_main_data to be '
|
|
|
|
|
f'{"non-empty" if expect_inherited else "empty"}, '
|
|
|
|
|
f'got: {actor._parent_main_data!r}'
|
2026-04-06 23:14:17 +00:00
|
|
|
)
|
2026-04-09 23:14:09 +00:00
|
|
|
return has_data
|
2026-04-06 23:14:17 +00:00
|
|
|
|
|
|
|
|
|
2026-04-06 22:32:50 +00:00
|
|
|
def test_run_in_actor_can_skip_parent_main_inheritance(
|
2026-04-10 16:03:59 +00:00
|
|
|
start_method: str, # <- only support on `trio` backend rn.
|
2026-04-06 06:19:51 +00:00
|
|
|
):
|
2026-04-09 23:14:09 +00:00
|
|
|
'''
|
|
|
|
|
Verify ``inherit_parent_main=False`` on ``run_in_actor()``
|
|
|
|
|
prevents parent ``__main__`` data from reaching the child.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-04-06 06:19:51 +00:00
|
|
|
if start_method != 'trio':
|
|
|
|
|
pytest.skip(
|
2026-04-10 16:03:59 +00:00
|
|
|
'parent main-inheritance opt-out only affects the trio backend'
|
2026-04-06 06:19:51 +00:00
|
|
|
)
|
2026-04-09 23:14:09 +00:00
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
async with tractor.open_nursery(start_method='trio') as an:
|
|
|
|
|
|
|
|
|
|
# Default: child receives parent __main__ bootstrap data
|
|
|
|
|
replaying = await an.run_in_actor(
|
|
|
|
|
check_parent_main_inheritance,
|
|
|
|
|
name='replaying-parent-main',
|
|
|
|
|
expect_inherited=True,
|
|
|
|
|
)
|
|
|
|
|
await replaying.result()
|
|
|
|
|
|
|
|
|
|
# Opt-out: child gets no parent __main__ data
|
|
|
|
|
isolated = await an.run_in_actor(
|
|
|
|
|
check_parent_main_inheritance,
|
|
|
|
|
name='isolated-parent-main',
|
|
|
|
|
inherit_parent_main=False,
|
|
|
|
|
expect_inherited=False,
|
|
|
|
|
)
|
|
|
|
|
await isolated.result()
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|
2026-04-06 22:32:50 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_start_actor_can_skip_parent_main_inheritance(
|
2026-04-10 16:03:59 +00:00
|
|
|
start_method: str, # <- only support on `trio` backend rn.
|
2026-04-06 22:32:50 +00:00
|
|
|
):
|
2026-04-09 23:14:09 +00:00
|
|
|
'''
|
|
|
|
|
Verify ``inherit_parent_main=False`` on ``start_actor()``
|
|
|
|
|
prevents parent ``__main__`` data from reaching the child.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-04-06 22:32:50 +00:00
|
|
|
if start_method != 'trio':
|
|
|
|
|
pytest.skip(
|
2026-04-10 16:03:59 +00:00
|
|
|
'parent main-inheritance opt-out only affects the trio backend'
|
2026-04-06 22:32:50 +00:00
|
|
|
)
|
2026-04-09 23:14:09 +00:00
|
|
|
|
|
|
|
|
async def main():
|
|
|
|
|
async with tractor.open_nursery(start_method='trio') as an:
|
|
|
|
|
|
|
|
|
|
# Default: child receives parent __main__ bootstrap data
|
|
|
|
|
replaying = await an.start_actor(
|
|
|
|
|
'replaying-parent-main',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
)
|
|
|
|
|
result = await replaying.run(
|
|
|
|
|
check_parent_main_inheritance,
|
|
|
|
|
expect_inherited=True,
|
|
|
|
|
)
|
|
|
|
|
assert result is True
|
|
|
|
|
await replaying.cancel_actor()
|
|
|
|
|
|
|
|
|
|
# Opt-out: child gets no parent __main__ data
|
|
|
|
|
isolated = await an.start_actor(
|
|
|
|
|
'isolated-parent-main',
|
|
|
|
|
enable_modules=[__name__],
|
|
|
|
|
inherit_parent_main=False,
|
|
|
|
|
)
|
|
|
|
|
result = await isolated.run(
|
|
|
|
|
check_parent_main_inheritance,
|
|
|
|
|
expect_inherited=False,
|
|
|
|
|
)
|
|
|
|
|
assert result is False
|
|
|
|
|
await isolated.cancel_actor()
|
|
|
|
|
|
|
|
|
|
trio.run(main)
|