2021-12-13 18:08:32 +00:00
|
|
|
# tractor: structured concurrent "actors".
|
|
|
|
# Copyright 2018-eternity Tyler Goodlet.
|
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
2021-11-22 18:28:30 +00:00
|
|
|
'''
|
2020-12-27 15:55:00 +00:00
|
|
|
Root actor runtime ignition(s).
|
2021-11-22 18:28:30 +00:00
|
|
|
|
|
|
|
'''
|
2025-04-02 02:21:51 +00:00
|
|
|
from contextlib import (
|
|
|
|
asynccontextmanager as acm,
|
|
|
|
)
|
2020-12-27 15:55:00 +00:00
|
|
|
from functools import partial
|
|
|
|
import importlib
|
2024-06-28 18:25:53 +00:00
|
|
|
import inspect
|
2021-11-17 19:45:24 +00:00
|
|
|
import logging
|
2024-04-09 17:58:51 +00:00
|
|
|
import os
|
2022-10-13 17:12:17 +00:00
|
|
|
import signal
|
2023-03-07 21:46:14 +00:00
|
|
|
import sys
|
2025-04-02 02:21:51 +00:00
|
|
|
from typing import (
|
|
|
|
Any,
|
|
|
|
Callable,
|
|
|
|
)
|
2020-12-27 16:51:38 +00:00
|
|
|
import warnings
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2022-10-09 17:12:50 +00:00
|
|
|
|
2020-12-27 15:55:00 +00:00
|
|
|
import trio
|
|
|
|
|
2025-06-16 19:37:21 +00:00
|
|
|
from . import _runtime
|
2025-05-15 16:41:16 +00:00
|
|
|
from .devx import (
|
|
|
|
debug,
|
|
|
|
_frame_stack,
|
2025-06-13 03:26:38 +00:00
|
|
|
pformat as _pformat,
|
2025-05-15 16:41:16 +00:00
|
|
|
)
|
2020-12-27 15:55:00 +00:00
|
|
|
from . import _spawn
|
|
|
|
from . import _state
|
|
|
|
from . import log
|
2025-03-23 03:14:04 +00:00
|
|
|
from .ipc import (
|
|
|
|
_connect_chan,
|
|
|
|
)
|
|
|
|
from ._addr import (
|
2025-04-02 02:21:51 +00:00
|
|
|
Address,
|
2025-03-31 01:36:45 +00:00
|
|
|
UnwrappedAddress,
|
|
|
|
default_lo_addrs,
|
|
|
|
mk_uuid,
|
|
|
|
wrap_address,
|
2025-03-23 03:14:04 +00:00
|
|
|
)
|
2025-06-13 03:16:29 +00:00
|
|
|
from .trionics import (
|
|
|
|
is_multi_cancelled,
|
2025-06-16 19:34:04 +00:00
|
|
|
collapse_eg,
|
2025-06-13 03:16:29 +00:00
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
from ._exceptions import (
|
2025-04-03 20:15:53 +00:00
|
|
|
RuntimeFailure,
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2020-12-27 15:55:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
logger = log.get_logger('tractor')
|
|
|
|
|
|
|
|
|
2025-05-13 16:13:12 +00:00
|
|
|
# TODO: stick this in a `@acm` defined in `devx.debug`?
|
2025-04-02 02:21:51 +00:00
|
|
|
# -[ ] also maybe consider making this a `wrapt`-deco to
|
|
|
|
# save an indent level?
|
|
|
|
#
|
2024-05-09 19:20:03 +00:00
|
|
|
@acm
|
2025-04-02 02:21:51 +00:00
|
|
|
async def maybe_block_bp(
|
|
|
|
debug_mode: bool,
|
|
|
|
maybe_enable_greenback: bool,
|
|
|
|
) -> bool:
|
|
|
|
# Override the global debugger hook to make it play nice with
|
|
|
|
# ``trio``, see much discussion in:
|
|
|
|
# https://github.com/python-trio/trio/issues/1155#issuecomment-742964018
|
|
|
|
builtin_bp_handler: Callable = sys.breakpointhook
|
|
|
|
orig_bp_path: str|None = os.environ.get(
|
|
|
|
'PYTHONBREAKPOINT',
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
bp_blocked: bool
|
|
|
|
if (
|
|
|
|
debug_mode
|
|
|
|
and maybe_enable_greenback
|
|
|
|
and (
|
2025-05-13 16:13:12 +00:00
|
|
|
maybe_mod := await debug.maybe_init_greenback(
|
2025-04-02 02:21:51 +00:00
|
|
|
raise_not_found=False,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
):
|
|
|
|
logger.info(
|
|
|
|
f'Found `greenback` installed @ {maybe_mod}\n'
|
2025-07-08 03:13:14 +00:00
|
|
|
f'Enabling `tractor.pause_from_sync()` support!\n'
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
|
|
|
os.environ['PYTHONBREAKPOINT'] = (
|
2025-05-13 16:13:12 +00:00
|
|
|
'tractor.devx.debug._sync_pause_from_builtin'
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
|
|
|
_state._runtime_vars['use_greenback'] = True
|
|
|
|
bp_blocked = False
|
2020-12-27 16:51:38 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
else:
|
|
|
|
# TODO: disable `breakpoint()` by default (without
|
|
|
|
# `greenback`) since it will break any multi-actor
|
|
|
|
# usage by a clobbered TTY's stdstreams!
|
|
|
|
def block_bps(*args, **kwargs):
|
|
|
|
raise RuntimeError(
|
|
|
|
'Trying to use `breakpoint()` eh?\n\n'
|
|
|
|
'Welp, `tractor` blocks `breakpoint()` built-in calls by default!\n'
|
|
|
|
'If you need to use it please install `greenback` and set '
|
|
|
|
'`debug_mode=True` when opening the runtime '
|
|
|
|
'(either via `.open_nursery()` or `open_root_actor()`)\n'
|
|
|
|
)
|
|
|
|
|
|
|
|
sys.breakpointhook = block_bps
|
|
|
|
# lol ok,
|
|
|
|
# https://docs.python.org/3/library/sys.html#sys.breakpointhook
|
|
|
|
os.environ['PYTHONBREAKPOINT'] = "0"
|
|
|
|
bp_blocked = True
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield bp_blocked
|
|
|
|
finally:
|
|
|
|
# restore any prior built-in `breakpoint()` hook state
|
|
|
|
if builtin_bp_handler is not None:
|
|
|
|
sys.breakpointhook = builtin_bp_handler
|
|
|
|
|
|
|
|
if orig_bp_path is not None:
|
|
|
|
os.environ['PYTHONBREAKPOINT'] = orig_bp_path
|
|
|
|
|
|
|
|
else:
|
|
|
|
# clear env back to having no entry
|
|
|
|
os.environ.pop('PYTHONBREAKPOINT', None)
|
|
|
|
|
|
|
|
|
2025-04-04 00:12:30 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
@acm
|
|
|
|
async def open_root_actor(
|
2023-01-13 21:57:55 +00:00
|
|
|
*,
|
2020-12-27 16:51:38 +00:00
|
|
|
# defaults are above
|
2025-03-31 01:36:45 +00:00
|
|
|
registry_addrs: list[UnwrappedAddress]|None = None,
|
2023-01-13 21:57:55 +00:00
|
|
|
|
|
|
|
# defaults are above
|
2025-03-31 01:36:45 +00:00
|
|
|
arbiter_addr: tuple[UnwrappedAddress]|None = None,
|
2025-03-23 03:14:04 +00:00
|
|
|
|
2025-04-04 00:12:30 +00:00
|
|
|
enable_transports: list[
|
2025-04-11 03:55:41 +00:00
|
|
|
# TODO, this should eventually be the pairs as
|
|
|
|
# defined by (codec, proto) as on `MsgTransport.
|
2025-04-04 00:12:30 +00:00
|
|
|
_state.TransportProtocolKey,
|
2025-04-11 03:55:41 +00:00
|
|
|
]|None = None,
|
2020-12-27 16:51:38 +00:00
|
|
|
|
2024-03-11 14:33:06 +00:00
|
|
|
name: str|None = 'root',
|
2020-12-27 16:51:38 +00:00
|
|
|
|
|
|
|
# either the `multiprocessing` start method:
|
|
|
|
# https://docs.python.org/3/library/multiprocessing.html#contexts-and-start-methods
|
|
|
|
# OR `trio` (the new default).
|
2024-03-11 14:33:06 +00:00
|
|
|
start_method: _spawn.SpawnMethodKey|None = None,
|
2020-12-27 16:51:38 +00:00
|
|
|
|
|
|
|
# enables the multi-process debugger support
|
2020-12-27 15:55:00 +00:00
|
|
|
debug_mode: bool = False,
|
2025-06-11 23:50:29 +00:00
|
|
|
maybe_enable_greenback: bool = False, # `.pause_from_sync()/breakpoint()` support
|
|
|
|
# ^XXX NOTE^ the perf implications of use,
|
|
|
|
# https://greenback.readthedocs.io/en/latest/principle.html#performance
|
2024-06-14 19:37:57 +00:00
|
|
|
enable_stack_on_sig: bool = False,
|
2020-12-27 16:51:38 +00:00
|
|
|
|
2021-01-03 02:34:39 +00:00
|
|
|
# internal logging
|
2024-03-11 14:33:06 +00:00
|
|
|
loglevel: str|None = None,
|
2021-01-03 02:34:39 +00:00
|
|
|
|
2024-03-11 14:33:06 +00:00
|
|
|
enable_modules: list|None = None,
|
|
|
|
rpc_module_paths: list|None = None,
|
2021-01-03 02:34:39 +00:00
|
|
|
|
2023-11-07 21:45:22 +00:00
|
|
|
# NOTE: allow caller to ensure that only one registry exists
|
|
|
|
# and that this call creates it.
|
|
|
|
ensure_registry: bool = False,
|
|
|
|
|
2024-05-22 19:11:21 +00:00
|
|
|
hide_tb: bool = True,
|
|
|
|
|
2025-05-13 16:13:12 +00:00
|
|
|
# XXX, proxied directly to `.devx.debug._maybe_enter_pm()`
|
2024-12-28 19:35:05 +00:00
|
|
|
# for REPL-entry logic.
|
|
|
|
debug_filter: Callable[
|
|
|
|
[BaseException|BaseExceptionGroup],
|
|
|
|
bool,
|
|
|
|
] = lambda err: not is_multi_cancelled(err),
|
|
|
|
|
2024-12-09 23:12:22 +00:00
|
|
|
# TODO, a way for actors to augment passing derived
|
|
|
|
# read-only state to sublayers?
|
|
|
|
# extra_rt_vars: dict|None = None,
|
|
|
|
|
2025-06-16 19:37:21 +00:00
|
|
|
) -> _runtime.Actor:
|
2022-10-13 17:12:17 +00:00
|
|
|
'''
|
2025-06-16 19:34:04 +00:00
|
|
|
Initialize the `tractor` runtime by starting a "root actor" in
|
|
|
|
a parent-most Python process.
|
|
|
|
|
|
|
|
All (disjoint) actor-process-trees-as-programs are created via
|
|
|
|
this entrypoint.
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2022-10-13 17:12:17 +00:00
|
|
|
'''
|
2025-04-02 02:21:51 +00:00
|
|
|
# XXX NEVER allow nested actor-trees!
|
2025-07-07 14:37:02 +00:00
|
|
|
if already_actor := _state.current_actor(
|
|
|
|
err_on_no_runtime=False,
|
|
|
|
):
|
2025-04-02 02:21:51 +00:00
|
|
|
rtvs: dict[str, Any] = _state._runtime_vars
|
|
|
|
root_mailbox: list[str, int] = rtvs['_root_mailbox']
|
|
|
|
registry_addrs: list[list[str, int]] = rtvs['_registry_addrs']
|
2025-04-03 20:15:53 +00:00
|
|
|
raise RuntimeFailure(
|
2025-04-02 02:21:51 +00:00
|
|
|
f'A current actor already exists !?\n'
|
|
|
|
f'({already_actor}\n'
|
|
|
|
f'\n'
|
|
|
|
f'You can NOT open a second root actor from within '
|
|
|
|
f'an existing tree and the current root of this '
|
|
|
|
f'already exists !!\n'
|
|
|
|
f'\n'
|
|
|
|
f'_root_mailbox: {root_mailbox!r}\n'
|
|
|
|
f'_registry_addrs: {registry_addrs!r}\n'
|
2024-03-22 20:41:49 +00:00
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
|
|
|
|
async with maybe_block_bp(
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
maybe_enable_greenback=maybe_enable_greenback,
|
2024-03-22 20:41:49 +00:00
|
|
|
):
|
2025-04-11 03:55:41 +00:00
|
|
|
if enable_transports is None:
|
|
|
|
enable_transports: list[str] = _state.current_ipc_protos()
|
2025-06-17 15:33:36 +00:00
|
|
|
else:
|
|
|
|
_state._runtime_vars['_enable_tpts'] = enable_transports
|
2025-04-11 03:55:41 +00:00
|
|
|
|
2025-06-17 15:33:36 +00:00
|
|
|
# TODO! support multi-tpts per actor!
|
|
|
|
# Bo
|
|
|
|
if not len(enable_transports) == 1:
|
|
|
|
raise RuntimeError(
|
|
|
|
f'No multi-tpt support yet!\n'
|
|
|
|
f'enable_transports={enable_transports!r}\n'
|
|
|
|
)
|
2025-04-11 03:55:41 +00:00
|
|
|
|
2025-05-15 16:41:16 +00:00
|
|
|
_frame_stack.hide_runtime_frames()
|
2025-04-02 02:21:51 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
2024-04-14 23:31:50 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# attempt to retreive ``trio``'s sigint handler and stash it
|
|
|
|
# on our debugger lock state.
|
2025-05-13 16:13:12 +00:00
|
|
|
debug.DebugStatus._trio_handler = signal.getsignal(signal.SIGINT)
|
2024-04-09 17:58:51 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# mark top most level process as root actor
|
|
|
|
_state._runtime_vars['_is_root'] = True
|
2021-02-16 00:23:53 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# caps based rpc list
|
|
|
|
enable_modules = (
|
|
|
|
enable_modules
|
|
|
|
or
|
|
|
|
[]
|
|
|
|
)
|
2022-10-13 17:12:17 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
if rpc_module_paths:
|
|
|
|
warnings.warn(
|
|
|
|
"`rpc_module_paths` is now deprecated, use "
|
|
|
|
" `enable_modules` instead.",
|
|
|
|
DeprecationWarning,
|
|
|
|
stacklevel=2,
|
|
|
|
)
|
|
|
|
enable_modules.extend(rpc_module_paths)
|
|
|
|
|
|
|
|
if start_method is not None:
|
|
|
|
_spawn.try_set_start_method(start_method)
|
|
|
|
|
|
|
|
# TODO! remove this ASAP!
|
|
|
|
if arbiter_addr is not None:
|
|
|
|
warnings.warn(
|
|
|
|
'`arbiter_addr` is now deprecated\n'
|
|
|
|
'Use `registry_addrs: list[tuple]` instead..',
|
|
|
|
DeprecationWarning,
|
|
|
|
stacklevel=2,
|
|
|
|
)
|
2025-07-07 14:37:02 +00:00
|
|
|
uw_reg_addrs = [arbiter_addr]
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-07-07 14:37:02 +00:00
|
|
|
uw_reg_addrs = registry_addrs
|
|
|
|
if not uw_reg_addrs:
|
|
|
|
uw_reg_addrs: list[UnwrappedAddress] = default_lo_addrs(
|
2025-04-02 02:21:51 +00:00
|
|
|
enable_transports
|
|
|
|
)
|
2021-01-05 13:28:06 +00:00
|
|
|
|
2025-07-07 14:37:02 +00:00
|
|
|
# must exist by now since all below code is dependent
|
|
|
|
assert uw_reg_addrs
|
|
|
|
registry_addrs: list[Address] = [
|
|
|
|
wrap_address(uw_addr)
|
|
|
|
for uw_addr in uw_reg_addrs
|
|
|
|
]
|
2021-01-03 02:34:39 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
loglevel = (
|
|
|
|
loglevel
|
|
|
|
or log._default_loglevel
|
|
|
|
).upper()
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
if (
|
|
|
|
debug_mode
|
2025-05-07 14:41:59 +00:00
|
|
|
and
|
|
|
|
_spawn._spawn_method == 'trio'
|
2025-04-02 02:21:51 +00:00
|
|
|
):
|
|
|
|
_state._runtime_vars['_debug_mode'] = True
|
2023-01-13 21:57:55 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# expose internal debug module to every actor allowing for
|
|
|
|
# use of ``await tractor.pause()``
|
2025-05-13 21:39:38 +00:00
|
|
|
enable_modules.append('tractor.devx.debug._tty_lock')
|
2025-03-23 03:14:04 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# if debug mode get's enabled *at least* use that level of
|
|
|
|
# logging for some informative console prompts.
|
|
|
|
if (
|
|
|
|
logging.getLevelName(
|
|
|
|
# lul, need the upper case for the -> int map?
|
|
|
|
# sweet "dynamic function behaviour" stdlib...
|
|
|
|
loglevel,
|
|
|
|
) > logging.getLevelName('PDB')
|
|
|
|
):
|
|
|
|
loglevel = 'PDB'
|
2021-11-22 18:28:30 +00:00
|
|
|
|
2024-02-20 18:23:16 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
elif debug_mode:
|
|
|
|
raise RuntimeError(
|
|
|
|
"Debug mode is only supported for the `trio` backend!"
|
|
|
|
)
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
assert loglevel
|
|
|
|
_log = log.get_console_log(loglevel)
|
|
|
|
assert _log
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# TODO: factor this into `.devx._stackscope`!!
|
2021-11-22 18:28:30 +00:00
|
|
|
if (
|
2025-04-02 02:21:51 +00:00
|
|
|
debug_mode
|
|
|
|
and
|
|
|
|
enable_stack_on_sig
|
2021-11-22 18:28:30 +00:00
|
|
|
):
|
2025-04-02 02:21:51 +00:00
|
|
|
from .devx._stackscope import enable_stack_on_sig
|
|
|
|
enable_stack_on_sig()
|
2021-07-08 17:02:33 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# closed into below ping task-func
|
2025-07-07 14:37:02 +00:00
|
|
|
ponged_addrs: list[Address] = []
|
2024-05-22 19:11:21 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
async def ping_tpt_socket(
|
2025-07-07 14:37:02 +00:00
|
|
|
addr: Address,
|
2025-04-02 02:21:51 +00:00
|
|
|
timeout: float = 1,
|
|
|
|
) -> None:
|
|
|
|
'''
|
|
|
|
Attempt temporary connection to see if a registry is
|
|
|
|
listening at the requested address by a tranport layer
|
|
|
|
ping.
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
If a connection can't be made quickly we assume none no
|
|
|
|
server is listening at that addr.
|
2024-02-20 18:23:16 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
'''
|
|
|
|
try:
|
|
|
|
# TODO: this connect-and-bail forces us to have to
|
|
|
|
# carefully rewrap TCP 104-connection-reset errors as
|
|
|
|
# EOF so as to avoid propagating cancel-causing errors
|
|
|
|
# to the channel-msg loop machinery. Likely it would
|
|
|
|
# be better to eventually have a "discovery" protocol
|
|
|
|
# with basic handshake instead?
|
|
|
|
with trio.move_on_after(timeout):
|
2025-07-07 14:37:02 +00:00
|
|
|
async with _connect_chan(addr.unwrap()):
|
2025-04-02 02:21:51 +00:00
|
|
|
ponged_addrs.append(addr)
|
|
|
|
|
|
|
|
except OSError:
|
2025-07-07 14:37:02 +00:00
|
|
|
# ?TODO, make this a "discovery" log level?
|
2025-04-02 02:21:51 +00:00
|
|
|
logger.info(
|
2025-07-07 14:37:02 +00:00
|
|
|
f'No root-actor registry found @ {addr!r}\n'
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2022-07-01 18:37:16 +00:00
|
|
|
|
2025-07-07 14:37:02 +00:00
|
|
|
# !TODO, this is basically just another (abstract)
|
|
|
|
# happy-eyeballs, so we should try for formalize it somewhere
|
|
|
|
# in a `.[_]discovery` ya?
|
|
|
|
#
|
2025-04-02 02:21:51 +00:00
|
|
|
async with trio.open_nursery() as tn:
|
2025-07-07 14:37:02 +00:00
|
|
|
for uw_addr in uw_reg_addrs:
|
|
|
|
addr: Address = wrap_address(uw_addr)
|
2025-04-02 02:21:51 +00:00
|
|
|
tn.start_soon(
|
|
|
|
ping_tpt_socket,
|
|
|
|
addr,
|
|
|
|
)
|
2023-09-27 19:19:30 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
trans_bind_addrs: list[UnwrappedAddress] = []
|
2023-09-27 19:19:30 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# Create a new local root-actor instance which IS NOT THE
|
|
|
|
# REGISTRAR
|
|
|
|
if ponged_addrs:
|
|
|
|
if ensure_registry:
|
|
|
|
raise RuntimeError(
|
|
|
|
f'Failed to open `{name}`@{ponged_addrs}: '
|
|
|
|
'registry socket(s) already bound'
|
|
|
|
)
|
2023-09-27 19:19:30 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# we were able to connect to an arbiter
|
2024-06-28 18:25:53 +00:00
|
|
|
logger.info(
|
2025-04-02 02:21:51 +00:00
|
|
|
f'Registry(s) seem(s) to exist @ {ponged_addrs}'
|
2024-06-28 18:25:53 +00:00
|
|
|
)
|
2023-09-27 19:19:30 +00:00
|
|
|
|
2025-06-16 19:37:21 +00:00
|
|
|
actor = _runtime.Actor(
|
2025-04-02 02:21:51 +00:00
|
|
|
name=name or 'anonymous',
|
|
|
|
uuid=mk_uuid(),
|
|
|
|
registry_addrs=ponged_addrs,
|
|
|
|
loglevel=loglevel,
|
|
|
|
enable_modules=enable_modules,
|
2023-10-19 16:40:37 +00:00
|
|
|
)
|
2025-07-07 14:37:02 +00:00
|
|
|
# **DO NOT** use the registry_addrs as the
|
|
|
|
# ipc-transport-server's bind-addrs as this is
|
|
|
|
# a new NON-registrar, ROOT-actor.
|
|
|
|
#
|
|
|
|
# XXX INSTEAD, bind random addrs using the same tpt
|
|
|
|
# proto.
|
2025-04-02 02:21:51 +00:00
|
|
|
for addr in ponged_addrs:
|
|
|
|
trans_bind_addrs.append(
|
2025-07-07 14:37:02 +00:00
|
|
|
addr.get_random(
|
|
|
|
bindspace=addr.bindspace,
|
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2023-09-27 19:19:30 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# Start this local actor as the "registrar", aka a regular
|
|
|
|
# actor who manages the local registry of "mailboxes" of
|
|
|
|
# other process-tree-local sub-actors.
|
|
|
|
else:
|
|
|
|
# NOTE that if the current actor IS THE REGISTAR, the
|
|
|
|
# following init steps are taken:
|
|
|
|
# - the tranport layer server is bound to each addr
|
|
|
|
# pair defined in provided registry_addrs, or the default.
|
2025-07-07 14:37:02 +00:00
|
|
|
trans_bind_addrs = uw_reg_addrs
|
2025-04-02 02:21:51 +00:00
|
|
|
|
|
|
|
# - it is normally desirable for any registrar to stay up
|
|
|
|
# indefinitely until either all registered (child/sub)
|
|
|
|
# actors are terminated (via SC supervision) or,
|
|
|
|
# a re-election process has taken place.
|
|
|
|
# NOTE: all of ^ which is not implemented yet - see:
|
|
|
|
# https://github.com/goodboy/tractor/issues/216
|
|
|
|
# https://github.com/goodboy/tractor/pull/348
|
|
|
|
# https://github.com/goodboy/tractor/issues/296
|
|
|
|
|
2025-06-16 19:37:21 +00:00
|
|
|
# TODO: rename as `RootActor` or is that even necessary?
|
|
|
|
actor = _runtime.Arbiter(
|
2025-04-02 02:21:51 +00:00
|
|
|
name=name or 'registrar',
|
|
|
|
uuid=mk_uuid(),
|
|
|
|
registry_addrs=registry_addrs,
|
|
|
|
loglevel=loglevel,
|
|
|
|
enable_modules=enable_modules,
|
2023-11-07 21:45:22 +00:00
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
# XXX, in case the root actor runtime was actually run from
|
|
|
|
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOt
|
|
|
|
# `.trio.run()`.
|
|
|
|
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
2023-11-07 21:45:22 +00:00
|
|
|
|
2025-07-07 14:37:02 +00:00
|
|
|
# NOTE, only set the loopback addr for the
|
|
|
|
# process-tree-global "root" mailbox since all sub-actors
|
|
|
|
# should be able to speak to their root actor over that
|
|
|
|
# channel.
|
|
|
|
raddrs: list[Address] = _state._runtime_vars['_root_addrs']
|
|
|
|
raddrs.extend(trans_bind_addrs)
|
|
|
|
# TODO, remove once we have also removed all usage;
|
|
|
|
# eventually all (root-)registry apis should expect > 1 addr.
|
|
|
|
_state._runtime_vars['_root_mailbox'] = raddrs[0]
|
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# Start up main task set via core actor-runtime nurseries.
|
|
|
|
try:
|
|
|
|
# assign process-local actor
|
|
|
|
_state._current_actor = actor
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
# start local channel-server and fake the portal API
|
|
|
|
# NOTE: this won't block since we provide the nursery
|
2025-07-07 14:37:02 +00:00
|
|
|
report: str = f'Starting actor-runtime for {actor.aid.reprol()!r}\n'
|
|
|
|
if reg_addrs := actor.registry_addrs:
|
|
|
|
report += (
|
|
|
|
'-> Opening new registry @ '
|
|
|
|
+
|
|
|
|
'\n'.join(
|
2025-07-08 03:13:14 +00:00
|
|
|
f'{addr}' for addr in reg_addrs
|
2025-07-07 14:37:02 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
logger.info(f'{report}\n')
|
2024-12-28 19:35:05 +00:00
|
|
|
|
2025-06-16 19:37:21 +00:00
|
|
|
# start runtime in a bg sub-task, yield to caller.
|
2025-06-16 19:34:04 +00:00
|
|
|
async with (
|
2025-08-20 14:44:42 +00:00
|
|
|
collapse_eg(),
|
2025-06-16 19:34:04 +00:00
|
|
|
trio.open_nursery() as root_tn,
|
2025-04-02 02:21:51 +00:00
|
|
|
|
2025-08-20 14:44:42 +00:00
|
|
|
# ?TODO? finally-footgun below?
|
2025-07-07 14:37:02 +00:00
|
|
|
# -> see note on why shielding.
|
|
|
|
# maybe_raise_from_masking_exc(),
|
|
|
|
):
|
Heh, add back `Actor._root_tn`, it has purpose..
Turns out I didn't read my own internals docs/comments and despite it
not being used previously, this adds the real use case: a root,
per-actor, scope which ensures parent comms are the last conc-thing to
be cancelled.
Also, the impl changes here make the test from 6410e45 (or wtv
it's rebased to) pass, i.e. we can support crash handling in the root
actor despite the root-tn having been (self) cancelled.
Superficial adjustments,
- rename `Actor._service_n` -> `._service_tn` everywhere.
- add asserts to `._runtime.async_main()` which ensure that the any
`.trionics.maybe_open_nursery()` calls against optionally passed
`._[root/service]_tn` are allocated-if-not-provided (the
`._service_tn`-case being an i-guess-prep-for-the-future-anti-pattern
Bp).
- obvi adjust all internal usage to match new naming.
Serious/real-use-case changes,
- add (back) a `Actor._root_tn` which sits a scope "above" the
service-tn and is either,
+ assigned in `._runtime.async_main()` for sub-actors OR,
+ assigned in `._root.open_root_actor()` for the root actor.
**THE primary reason** to keep this "upper" tn is that during
a full-`Actor`-cancellation condition (more details below) we want to
ensure that the IPC connection with a sub-actor's parent is **the last
thing to be cancelled**; this is most simply implemented by ensuring
that the `Actor._parent_chan: .ipc.Channel` is handled in an upper
scope in `_rpc.process_messages()`-subtask-terms.
- for the root actor this `root_tn` is allocated in `.open_root_actor()`
body and assigned as such.
- extend `Actor.cancel_soon()` to be cohesive with this entire teardown
"policy" by scheduling a task in the `._root_tn` which,
* waits for the `._service_tn` to complete and then,
* cancels the `._root_tn.cancel_scope`,
* includes "sclangy" console logging throughout.
2025-08-19 23:24:20 +00:00
|
|
|
actor._root_tn = root_tn
|
2025-06-16 19:37:21 +00:00
|
|
|
# `_runtime.async_main()` creates an internal nursery
|
2025-04-02 02:21:51 +00:00
|
|
|
# and blocks here until any underlying actor(-process)
|
|
|
|
# tree has terminated thereby conducting so called
|
|
|
|
# "end-to-end" structured concurrency throughout an
|
|
|
|
# entire hierarchical python sub-process set; all
|
|
|
|
# "actor runtime" primitives are SC-compat and thus all
|
|
|
|
# transitively spawned actors/processes must be as
|
|
|
|
# well.
|
2025-06-16 19:34:04 +00:00
|
|
|
await root_tn.start(
|
2025-04-02 02:21:51 +00:00
|
|
|
partial(
|
2025-06-16 19:37:21 +00:00
|
|
|
_runtime.async_main,
|
2025-04-02 02:21:51 +00:00
|
|
|
actor,
|
|
|
|
accept_addrs=trans_bind_addrs,
|
|
|
|
parent_addr=None
|
2024-12-28 19:35:05 +00:00
|
|
|
)
|
2024-03-05 17:30:09 +00:00
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
try:
|
|
|
|
yield actor
|
|
|
|
except (
|
|
|
|
Exception,
|
|
|
|
BaseExceptionGroup,
|
|
|
|
) as err:
|
|
|
|
|
|
|
|
# TODO, in beginning to handle the subsubactor with
|
|
|
|
# crashed grandparent cases..
|
|
|
|
#
|
2025-05-13 16:13:12 +00:00
|
|
|
# was_locked: bool = await debug.maybe_wait_for_debugger(
|
2025-04-02 02:21:51 +00:00
|
|
|
# child_in_debug=True,
|
|
|
|
# )
|
|
|
|
# XXX NOTE XXX see equiv note inside
|
|
|
|
# `._runtime.Actor._stream_handler()` where in the
|
|
|
|
# non-root or root-that-opened-this-mahually case we
|
|
|
|
# wait for the local actor-nursery to exit before
|
|
|
|
# exiting the transport channel handler.
|
2025-05-13 16:13:12 +00:00
|
|
|
entered: bool = await debug._maybe_enter_pm(
|
2025-04-02 02:21:51 +00:00
|
|
|
err,
|
|
|
|
api_frame=inspect.currentframe(),
|
|
|
|
debug_filter=debug_filter,
|
2025-08-10 19:03:15 +00:00
|
|
|
|
|
|
|
# XXX NOTE, required to debug root-actor
|
|
|
|
# crashes under cancellation conditions; so
|
|
|
|
# most of them!
|
|
|
|
shield=root_tn.cancel_scope.cancel_called,
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2023-03-07 21:46:14 +00:00
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
if (
|
|
|
|
not entered
|
|
|
|
and
|
|
|
|
not is_multi_cancelled(
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
):
|
|
|
|
logger.exception(
|
|
|
|
'Root actor crashed\n'
|
|
|
|
f'>x)\n'
|
|
|
|
f' |_{actor}\n'
|
|
|
|
)
|
|
|
|
|
|
|
|
# ALWAYS re-raise any error bubbled up from the
|
|
|
|
# runtime!
|
|
|
|
raise
|
|
|
|
|
|
|
|
finally:
|
2025-07-08 03:13:14 +00:00
|
|
|
# NOTE/TODO?, not sure if we'll ever need this but it's
|
2025-04-02 02:21:51 +00:00
|
|
|
# possibly better for even more determinism?
|
|
|
|
# logger.cancel(
|
|
|
|
# f'Waiting on {len(nurseries)} nurseries in root..')
|
|
|
|
# nurseries = actor._actoruid2nursery.values()
|
|
|
|
# async with trio.open_nursery() as tempn:
|
|
|
|
# for an in nurseries:
|
|
|
|
# tempn.start_soon(an.exited.wait)
|
|
|
|
|
2025-06-13 03:26:38 +00:00
|
|
|
op_nested_actor_repr: str = _pformat.nest_from_op(
|
|
|
|
input_op='>) ',
|
2025-07-14 21:51:45 +00:00
|
|
|
text=actor.pformat(),
|
2025-06-13 03:26:38 +00:00
|
|
|
nest_prefix='|_',
|
|
|
|
)
|
2025-04-02 02:21:51 +00:00
|
|
|
logger.info(
|
|
|
|
f'Closing down root actor\n'
|
2025-07-07 14:37:02 +00:00
|
|
|
f'{op_nested_actor_repr}'
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2025-07-07 14:37:02 +00:00
|
|
|
# XXX, THIS IS A *finally-footgun*!
|
2025-08-20 14:44:42 +00:00
|
|
|
# (also mentioned in with-block above)
|
2025-07-07 14:37:02 +00:00
|
|
|
# -> though already shields iternally it can
|
|
|
|
# taskc here and mask underlying errors raised in
|
|
|
|
# the try-block above?
|
|
|
|
with trio.CancelScope(shield=True):
|
|
|
|
await actor.cancel(None) # self cancel
|
2025-04-02 02:21:51 +00:00
|
|
|
finally:
|
2025-05-07 14:41:59 +00:00
|
|
|
# revert all process-global runtime state
|
|
|
|
if (
|
|
|
|
debug_mode
|
|
|
|
and
|
|
|
|
_spawn._spawn_method == 'trio'
|
|
|
|
):
|
|
|
|
_state._runtime_vars['_debug_mode'] = False
|
|
|
|
|
2025-04-02 02:21:51 +00:00
|
|
|
_state._current_actor = None
|
|
|
|
_state._last_actor_terminated = actor
|
2025-05-07 14:41:59 +00:00
|
|
|
|
2025-07-07 14:37:02 +00:00
|
|
|
sclang_repr: str = _pformat.nest_from_op(
|
|
|
|
input_op=')>',
|
|
|
|
text=actor.pformat(),
|
|
|
|
nest_prefix='|_',
|
|
|
|
nest_indent=1,
|
|
|
|
)
|
|
|
|
|
|
|
|
logger.info(
|
2025-04-02 02:21:51 +00:00
|
|
|
f'Root actor terminated\n'
|
2025-07-07 14:37:02 +00:00
|
|
|
f'{sclang_repr}'
|
2025-04-02 02:21:51 +00:00
|
|
|
)
|
2020-12-27 15:55:00 +00:00
|
|
|
|
|
|
|
|
2022-09-15 20:15:17 +00:00
|
|
|
def run_daemon(
|
|
|
|
enable_modules: list[str],
|
2020-12-27 15:55:00 +00:00
|
|
|
|
|
|
|
# runtime kwargs
|
2023-01-26 17:43:06 +00:00
|
|
|
name: str | None = 'root',
|
2025-03-31 01:36:45 +00:00
|
|
|
registry_addrs: list[UnwrappedAddress]|None = None,
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2023-01-26 17:43:06 +00:00
|
|
|
start_method: str | None = None,
|
2020-12-27 15:55:00 +00:00
|
|
|
debug_mode: bool = False,
|
2025-02-26 00:37:30 +00:00
|
|
|
|
|
|
|
# TODO, support `infected_aio=True` mode by,
|
|
|
|
# - calling the appropriate entrypoint-func from `.to_asyncio`
|
|
|
|
# - maybe init-ing `greenback` as done above in
|
|
|
|
# `open_root_actor()`.
|
|
|
|
|
2022-09-15 20:15:17 +00:00
|
|
|
**kwargs
|
2020-12-27 15:55:00 +00:00
|
|
|
|
2022-09-15 20:15:17 +00:00
|
|
|
) -> None:
|
|
|
|
'''
|
2025-02-26 00:37:30 +00:00
|
|
|
Spawn a root (daemon) actor which will respond to RPC; the main
|
|
|
|
task simply starts the runtime and then blocks via embedded
|
|
|
|
`trio.sleep_forever()`.
|
2022-09-15 20:15:17 +00:00
|
|
|
|
|
|
|
This is a very minimal convenience wrapper around starting
|
|
|
|
a "run-until-cancelled" root actor which can be started with a set
|
|
|
|
of enabled modules for RPC request handling.
|
|
|
|
|
|
|
|
'''
|
|
|
|
kwargs['enable_modules'] = list(enable_modules)
|
|
|
|
|
|
|
|
for path in enable_modules:
|
|
|
|
importlib.import_module(path)
|
2020-12-27 15:55:00 +00:00
|
|
|
|
|
|
|
async def _main():
|
|
|
|
async with open_root_actor(
|
2023-09-27 19:19:30 +00:00
|
|
|
registry_addrs=registry_addrs,
|
2020-12-27 15:55:00 +00:00
|
|
|
name=name,
|
|
|
|
start_method=start_method,
|
|
|
|
debug_mode=debug_mode,
|
|
|
|
**kwargs,
|
|
|
|
):
|
2022-09-15 20:15:17 +00:00
|
|
|
return await trio.sleep_forever()
|
2020-12-27 15:55:00 +00:00
|
|
|
|
|
|
|
return trio.run(_main)
|