-
Notifications
You must be signed in to change notification settings - Fork 15
Add per-actor parent-main inheritance opt-out #434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f5301d3
6309c2e
83b6c42
ea971d2
0063776
b883b27
c6c591e
acf6568
656c6c3
e8f1eca
27bf566
a0a7668
570c975
ca1b01f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import os | ||
|
|
||
|
|
||
| async def child_fn() -> str: | ||
| return f"child OK pid={os.getpid()}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| """ | ||
| Integration test: spawning tractor actors from an MPI process. | ||
|
|
||
| When a parent is launched via ``mpirun``, Open MPI sets ``OMPI_*`` env | ||
| vars that bind ``MPI_Init`` to the ``orted`` daemon. Tractor children | ||
| inherit those env vars, so if ``inherit_parent_main=True`` (the default) | ||
| the child re-executes ``__main__``, re-imports ``mpi4py``, and | ||
| ``MPI_Init_thread`` fails because the child was never spawned by | ||
| ``orted``:: | ||
|
|
||
| getting local rank failed | ||
| --> Returned value No permission (-17) instead of ORTE_SUCCESS | ||
|
|
||
| Passing ``inherit_parent_main=False`` and placing RPC functions in a | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh nice so then this all works given the #434 patchset yah? |
||
| separate importable module (``_child``) avoids the re-import entirely. | ||
|
|
||
| Usage:: | ||
|
|
||
| mpirun --allow-run-as-root -np 1 python -m \ | ||
| examples.integration.mpi4py.inherit_parent_main | ||
| """ | ||
|
|
||
| from mpi4py import MPI | ||
|
|
||
| import os | ||
| import trio | ||
| import tractor | ||
|
|
||
| from ._child import child_fn | ||
|
|
||
|
|
||
| async def main() -> None: | ||
| rank = MPI.COMM_WORLD.Get_rank() | ||
| print(f"[parent] rank={rank} pid={os.getpid()}", flush=True) | ||
|
|
||
| async with tractor.open_nursery(start_method='trio') as an: | ||
| portal = await an.start_actor( | ||
| 'mpi-child', | ||
| enable_modules=[child_fn.__module__], | ||
| # Without this the child replays __main__, which | ||
| # re-imports mpi4py and crashes on MPI_Init. | ||
| inherit_parent_main=False, | ||
| ) | ||
| result = await portal.run(child_fn) | ||
| print(f"[parent] got: {result}", flush=True) | ||
| await portal.cancel_actor() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| trio.run(main) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,12 @@ | ||
| """ | ||
| Spawning basics | ||
| Spawning basics including audit of, | ||
|
|
||
| - subproc bootstrap, such as subactor runtime-data/config inheritance, | ||
| - basic (and mostly legacy) `ActorNursery` subactor starting and | ||
| cancel APIs. | ||
|
|
||
| Simple (and generally legacy) examples from the original | ||
| API design. | ||
|
|
||
| """ | ||
| from functools import partial | ||
|
|
@@ -98,7 +105,9 @@ async def movie_theatre_question(): | |
|
|
||
|
|
||
| @tractor_test | ||
| async def test_movie_theatre_convo(start_method): | ||
| async def test_movie_theatre_convo( | ||
| start_method: str, | ||
| ): | ||
| ''' | ||
| The main ``tractor`` routine. | ||
|
|
||
|
|
@@ -151,13 +160,16 @@ async def test_most_beautiful_word( | |
| name='some_linguist', | ||
| ) | ||
|
|
||
| print(await portal.result()) | ||
| res: Any = await portal.wait_for_result() | ||
| assert res == return_value | ||
| # The ``async with`` will unblock here since the 'some_linguist' | ||
| # actor has completed its main task ``cellar_door``. | ||
|
|
||
| # this should pull the cached final result already captured during | ||
| # the nursery block exit. | ||
| print(await portal.result()) | ||
| res: Any = await portal.wait_for_result() | ||
| assert res == return_value | ||
| print(res) | ||
|
|
||
|
|
||
| async def check_loglevel(level): | ||
|
|
@@ -168,16 +180,24 @@ async def check_loglevel(level): | |
| log.critical('yoyoyo') | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| 'level', [ | ||
| 'debug', | ||
| 'cancel', | ||
| 'critical' | ||
| ], | ||
| ids='loglevel={}'.format, | ||
| ) | ||
| def test_loglevel_propagated_to_subactor( | ||
| start_method, | ||
| capfd, | ||
| reg_addr, | ||
| 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?") | ||
|
|
||
| level = 'critical' | ||
| "a bug with `capfd` seems to make forkserver capture not work?" | ||
| ) | ||
|
|
||
| async def main(): | ||
| async with tractor.open_nursery( | ||
|
|
@@ -197,3 +217,121 @@ async def main(): | |
| # ensure subactor spits log message on stderr | ||
| captured = capfd.readouterr() | ||
| assert 'yoyoyo' in captured.err | ||
|
|
||
|
|
||
| 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. | ||
|
|
||
| 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. | ||
|
|
||
| ''' | ||
| import tractor | ||
| actor: tractor.Actor = tractor.current_actor() | ||
| 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}' | ||
| ) | ||
| return has_data | ||
|
|
||
|
|
||
| def test_run_in_actor_can_skip_parent_main_inheritance( | ||
| start_method: str, # <- only support on `trio` backend rn. | ||
| ): | ||
| ''' | ||
| Verify ``inherit_parent_main=False`` on ``run_in_actor()`` | ||
| prevents parent ``__main__`` data from reaching the child. | ||
|
|
||
| ''' | ||
| if start_method != 'trio': | ||
| pytest.skip( | ||
| 'parent main-inheritance opt-out only affects the trio backend' | ||
| ) | ||
|
|
||
| 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() | ||
|
|
||
|
Comment on lines
+277
to
+292
|
||
| trio.run(main) | ||
|
|
||
|
|
||
| def test_start_actor_can_skip_parent_main_inheritance( | ||
| start_method: str, # <- only support on `trio` backend rn. | ||
| ): | ||
| ''' | ||
| Verify ``inherit_parent_main=False`` on ``start_actor()`` | ||
| prevents parent ``__main__`` data from reaching the child. | ||
|
|
||
| ''' | ||
| if start_method != 'trio': | ||
| pytest.skip( | ||
| 'parent main-inheritance opt-out only affects the trio backend' | ||
| ) | ||
|
|
||
| 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh nice, it might be worth tasking us in follow up to be able dynamically write modules like this from an internal API much like what
pytestoffers?something like their
tmp_pathfixture maybe,Also relates to our wishlist for #360
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe give #441 a quick lookover to be sure i didn't miss/get-anything-wrong if you can as well 🙏🏼