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/>.
|
|
|
|
|
|
2018-07-14 20:09:05 +00:00
|
|
|
"""
|
|
|
|
|
``trio`` inspired apis and helpers
|
2021-12-13 18:08:32 +00:00
|
|
|
|
2018-07-14 20:09:05 +00:00
|
|
|
"""
|
2022-10-09 17:12:50 +00:00
|
|
|
from contextlib import asynccontextmanager as acm
|
2020-06-28 17:10:02 +00:00
|
|
|
from functools import partial
|
2021-04-28 15:39:26 +00:00
|
|
|
import inspect
|
2025-03-31 01:36:45 +00:00
|
|
|
from typing import (
|
|
|
|
|
TYPE_CHECKING,
|
|
|
|
|
)
|
2018-08-20 02:13:13 +00:00
|
|
|
import typing
|
2021-01-05 13:28:06 +00:00
|
|
|
import warnings
|
2018-07-14 20:09:05 +00:00
|
|
|
|
|
|
|
|
import trio
|
|
|
|
|
|
2025-03-23 03:14:04 +00:00
|
|
|
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from ..devx import (
|
2025-07-07 14:59:00 +00:00
|
|
|
debug,
|
|
|
|
|
pformat as _pformat,
|
|
|
|
|
)
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from ..discovery._addr import (
|
2025-03-31 01:36:45 +00:00
|
|
|
UnwrappedAddress,
|
|
|
|
|
mk_uuid,
|
2025-03-23 03:14:04 +00:00
|
|
|
)
|
2026-05-07 22:01:59 +00:00
|
|
|
from ._state import (
|
|
|
|
|
current_actor,
|
|
|
|
|
is_main_process,
|
|
|
|
|
)
|
|
|
|
|
from ..log import (
|
|
|
|
|
get_logger,
|
|
|
|
|
get_loglevel,
|
|
|
|
|
)
|
2022-08-03 18:46:53 +00:00
|
|
|
from ._runtime import Actor
|
2018-07-14 20:09:05 +00:00
|
|
|
from ._portal import Portal
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from ..trionics import (
|
2024-03-01 20:44:01 +00:00
|
|
|
is_multi_cancelled,
|
2025-06-16 17:23:54 +00:00
|
|
|
collapse_eg,
|
2025-06-13 03:16:29 +00:00
|
|
|
)
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from .._exceptions import (
|
2026-05-07 22:01:59 +00:00
|
|
|
ActorTooSlowError,
|
2024-03-01 20:44:01 +00:00
|
|
|
ContextCancelled,
|
|
|
|
|
)
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from .._root import (
|
2025-04-04 00:12:30 +00:00
|
|
|
open_root_actor,
|
|
|
|
|
)
|
2020-07-23 17:23:55 +00:00
|
|
|
from . import _state
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
from ..spawn import _spawn
|
2018-07-14 20:09:05 +00:00
|
|
|
|
|
|
|
|
|
2022-02-16 17:08:35 +00:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
import multiprocessing as mp
|
Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:
- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
`_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
`_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`
Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.
Also,
- update all intra-pkg imports across ~35 mods to use
the new subpkg paths (e.g. `from .runtime._state`
instead of `from ._state`)
(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:42:16 +00:00
|
|
|
# from ..ipc._server import IPCServer
|
|
|
|
|
from ..ipc import IPCServer
|
2026-05-07 22:01:59 +00:00
|
|
|
from ..spawn._spawn import ProcessType
|
2025-04-11 20:55:03 +00:00
|
|
|
|
2022-02-16 17:08:35 +00:00
|
|
|
|
2026-02-09 18:15:47 +00:00
|
|
|
log = get_logger()
|
2018-07-14 20:09:05 +00:00
|
|
|
|
|
|
|
|
|
2026-05-07 22:01:59 +00:00
|
|
|
async def _try_cancel_then_kill(
|
|
|
|
|
portal: Portal,
|
|
|
|
|
# `ProcessType` is `TYPE_CHECKING`-only (defined under that
|
|
|
|
|
# guard in `..spawn._spawn`) so we stringify here to avoid
|
|
|
|
|
# eager runtime eval of the annotation at function-def time
|
|
|
|
|
# (this module has no `from __future__ import annotations`).
|
|
|
|
|
proc: 'ProcessType',
|
|
|
|
|
subactor: Actor,
|
|
|
|
|
debug_mode_active: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Per-child cancel-then-escalate helper used by
|
|
|
|
|
`ActorNursery.cancel()`.
|
|
|
|
|
|
|
|
|
|
Sends a graceful actor-runtime cancel-RPC via
|
|
|
|
|
`Portal.cancel_actor(raise_on_timeout=True)`. If the bounded-wait
|
|
|
|
|
expires before the peer ack's, `ActorTooSlowError` is raised and
|
|
|
|
|
we escalate via `proc.terminate()` (SIGTERM) per SC-discipline:
|
|
|
|
|
|
|
|
|
|
graceful cancel-req -> bounded wait -> hard-kill
|
|
|
|
|
|
|
|
|
|
Without this escalation, a same-name sibling subactor whose
|
|
|
|
|
cancel-RPC failed to ack within `Portal.cancel_timeout` (e.g.
|
|
|
|
|
under TCP+forkserver register-RPC contention) would park the
|
|
|
|
|
parent's `soft_kill()` watcher forever waiting on `proc.poll()`,
|
|
|
|
|
deadlocking nursery `__aexit__`. See `ActorTooSlowError` for
|
|
|
|
|
the wider write-up.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
# XXX, do NOT escalate to `proc.terminate()` while ANY of
|
|
|
|
|
# the following are true — SIGTERM-ing a sub would tear
|
|
|
|
|
# down its sub-tree including any descendant proxying
|
|
|
|
|
# stdio to/from a REPL-locked actor, clobbering the user's
|
|
|
|
|
# debug session:
|
|
|
|
|
#
|
|
|
|
|
# - `Lock.ctx_in_debug is not None`: most precise — some
|
|
|
|
|
# actor in the tree is currently REPL-locked. Set in the
|
|
|
|
|
# root actor for the lifetime of the lock. Raceable
|
|
|
|
|
# (false negative if SIGINT arrives before lock-acquire
|
|
|
|
|
# RPC completes).
|
|
|
|
|
#
|
|
|
|
|
# - `_runtime_vars['_debug_mode']`: root-actor was opened
|
|
|
|
|
# with `debug_mode=True` (via `open_root_actor` /
|
|
|
|
|
# `open_nursery`). Set once at root boot, never cleared.
|
|
|
|
|
# Catches deep-descendant REPL sessions even when the
|
|
|
|
|
# intermediate nurseries didn't pass `debug_mode=` per-
|
|
|
|
|
# child.
|
|
|
|
|
#
|
|
|
|
|
# - `debug_mode_active`: this nursery has at least one
|
|
|
|
|
# child started with an explicit `debug_mode=` arg
|
|
|
|
|
# (`ActorNursery._at_least_one_child_in_debug`). Catches
|
|
|
|
|
# the case where root is NOT in debug-mode but a
|
|
|
|
|
# nursery-direct child opted in.
|
|
|
|
|
#
|
|
|
|
|
# Independent because root may NOT be in debug-mode even
|
|
|
|
|
# when a child is (only the child's `_runtime_vars` is
|
|
|
|
|
# mutated by per-child `debug_mode=True`). ORing covers
|
|
|
|
|
# every flavor without false-positively skipping
|
|
|
|
|
# legitimate hard-kill paths in non-debug trees.
|
|
|
|
|
if (
|
|
|
|
|
debug.Lock.ctx_in_debug is not None
|
|
|
|
|
or
|
|
|
|
|
_state._runtime_vars.get('_debug_mode', False)
|
|
|
|
|
or
|
|
|
|
|
debug_mode_active
|
|
|
|
|
):
|
|
|
|
|
await portal.cancel_actor()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await portal.cancel_actor(raise_on_timeout=True)
|
|
|
|
|
except ActorTooSlowError as too_slow:
|
|
|
|
|
log.error(
|
|
|
|
|
f'Cancel-ack TIMED OUT for sub-actor\n'
|
|
|
|
|
f' uid: {subactor.aid.reprol()!r}\n'
|
|
|
|
|
f' reason: {too_slow}\n'
|
|
|
|
|
f'-> escalating to `proc.terminate()` (hard-kill)\n'
|
|
|
|
|
)
|
|
|
|
|
proc.terminate()
|
|
|
|
|
|
|
|
|
|
|
2018-07-14 20:09:05 +00:00
|
|
|
class ActorNursery:
|
2021-11-07 21:45:00 +00:00
|
|
|
'''
|
|
|
|
|
The fundamental actor supervision construct: spawn and manage
|
|
|
|
|
explicit lifetime and capability restricted, bootstrapped,
|
|
|
|
|
``trio.run()`` scheduled sub-processes.
|
|
|
|
|
|
|
|
|
|
Though the concept of a "process nursery" is different in complexity
|
|
|
|
|
and slightly different in semantics then a tradtional single
|
|
|
|
|
threaded task nursery, much of the interface is the same. New
|
|
|
|
|
processes each require a top level "parent" or "root" task which is
|
|
|
|
|
itself no different then any task started by a tradtional
|
|
|
|
|
``trio.Nursery``. The main difference is that each "actor" (a
|
|
|
|
|
process + ``trio.run()``) contains a full, paralell executing
|
|
|
|
|
``trio``-task-tree. The following super powers ensue:
|
|
|
|
|
|
|
|
|
|
- starting tasks in a child actor are completely independent of
|
|
|
|
|
tasks started in the current process. They execute in *parallel*
|
|
|
|
|
relative to tasks in the current process and are scheduled by their
|
|
|
|
|
own actor's ``trio`` run loop.
|
|
|
|
|
- tasks scheduled in a remote process still maintain an SC protocol
|
|
|
|
|
across memory boundaries using a so called "structured concurrency
|
|
|
|
|
dialogue protocol" which ensures task-hierarchy-lifetimes are linked.
|
|
|
|
|
- remote tasks (in another actor) can fail and relay failure back to
|
|
|
|
|
the caller task (in some other actor) via a seralized
|
|
|
|
|
``RemoteActorError`` which means no zombie process or RPC
|
|
|
|
|
initiated task can ever go off on its own.
|
|
|
|
|
|
|
|
|
|
'''
|
2020-01-20 16:10:51 +00:00
|
|
|
def __init__(
|
|
|
|
|
self,
|
2024-06-27 20:25:46 +00:00
|
|
|
# TODO: maybe def these as fields of a struct looking type?
|
2020-01-20 16:10:51 +00:00
|
|
|
actor: Actor,
|
|
|
|
|
ria_nursery: trio.Nursery,
|
|
|
|
|
da_nursery: trio.Nursery,
|
2022-10-13 19:42:33 +00:00
|
|
|
errors: dict[tuple[str, str], BaseException],
|
2024-05-20 21:04:30 +00:00
|
|
|
|
2020-01-20 16:10:51 +00:00
|
|
|
) -> None:
|
2018-08-20 02:13:13 +00:00
|
|
|
# self.supervisor = supervisor # TODO
|
2018-08-31 21:16:24 +00:00
|
|
|
self._actor: Actor = actor
|
2024-06-27 20:25:46 +00:00
|
|
|
|
|
|
|
|
# TODO: rename to `._tn` for our conventional "task-nursery"
|
2020-01-20 16:10:51 +00:00
|
|
|
self._da_nursery = da_nursery
|
2024-06-27 20:25:46 +00:00
|
|
|
|
2022-09-15 20:56:50 +00:00
|
|
|
self._children: dict[
|
|
|
|
|
tuple[str, str],
|
2022-10-09 19:09:14 +00:00
|
|
|
tuple[
|
|
|
|
|
Actor,
|
2022-10-09 20:05:40 +00:00
|
|
|
trio.Process | mp.Process,
|
2023-09-27 19:19:30 +00:00
|
|
|
Portal | None,
|
2022-10-09 19:09:14 +00:00
|
|
|
]
|
2018-08-31 21:16:24 +00:00
|
|
|
] = {}
|
2024-06-27 20:25:46 +00:00
|
|
|
|
2020-01-20 16:10:51 +00:00
|
|
|
self._join_procs = trio.Event()
|
2021-12-09 22:51:36 +00:00
|
|
|
self._at_least_one_child_in_debug: bool = False
|
2020-01-20 16:10:51 +00:00
|
|
|
self.errors = errors
|
2024-05-20 21:04:30 +00:00
|
|
|
self._scope_error: BaseException|None = None
|
2024-06-27 20:25:46 +00:00
|
|
|
self.exited = trio.Event()
|
2018-07-14 20:09:05 +00:00
|
|
|
|
2024-03-01 20:44:01 +00:00
|
|
|
# NOTE: when no explicit call is made to
|
|
|
|
|
# `.open_root_actor()` by application code,
|
|
|
|
|
# `.open_nursery()` will implicitly call it to start the
|
|
|
|
|
# actor-tree runtime. In this case we mark ourselves as
|
|
|
|
|
# such so that runtime components can be aware for logging
|
|
|
|
|
# and syncing purposes to any actor opened nurseries.
|
|
|
|
|
self._implicit_runtime_started: bool = False
|
|
|
|
|
|
2024-06-27 20:25:46 +00:00
|
|
|
# TODO: remove the `.run_in_actor()` API and thus this 2ndary
|
|
|
|
|
# nursery when that API get's moved outside this primitive!
|
|
|
|
|
self._ria_nursery = ria_nursery
|
2025-07-15 23:29:38 +00:00
|
|
|
|
|
|
|
|
# TODO, factor this into a .hilevel api!
|
|
|
|
|
#
|
2024-06-27 20:25:46 +00:00
|
|
|
# portals spawned with ``run_in_actor()`` are
|
|
|
|
|
# cancelled when their "main" result arrives
|
|
|
|
|
self._cancel_after_result_on_exit: set = set()
|
|
|
|
|
|
2025-07-15 23:29:38 +00:00
|
|
|
# trio.Nursery-like cancel (request) statuses
|
|
|
|
|
self._cancelled_caught: bool = False
|
|
|
|
|
self._cancel_called: bool = False
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def cancel_called(self) -> bool:
|
|
|
|
|
'''
|
|
|
|
|
Records whether cancellation has been requested for this
|
|
|
|
|
actor-nursery by a call to `.cancel()` either due to,
|
|
|
|
|
- an explicit call by some actor-local-task,
|
|
|
|
|
- an implicit call due to an error/cancel emited inside
|
|
|
|
|
the `tractor.open_nursery()` block.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
return self._cancel_called
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def cancelled_caught(self) -> bool:
|
|
|
|
|
'''
|
|
|
|
|
Set when this nursery was able to cance all spawned subactors
|
|
|
|
|
gracefully via an (implicit) call to `.cancel()`.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
return self._cancelled_caught
|
|
|
|
|
|
|
|
|
|
# TODO! remove internal/test-suite usage!
|
|
|
|
|
@property
|
|
|
|
|
def cancelled(self) -> bool:
|
|
|
|
|
warnings.warn(
|
|
|
|
|
"`ActorNursery.cancelled` is now deprecated, use "
|
|
|
|
|
" `.cancel_called` instead.",
|
|
|
|
|
DeprecationWarning,
|
|
|
|
|
stacklevel=2,
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
self._cancel_called
|
|
|
|
|
# and
|
|
|
|
|
# self._cancelled_caught
|
|
|
|
|
)
|
|
|
|
|
|
2018-07-14 20:09:05 +00:00
|
|
|
async def start_actor(
|
|
|
|
|
self,
|
|
|
|
|
name: str,
|
2024-05-20 21:04:30 +00:00
|
|
|
|
2020-07-20 20:08:03 +00:00
|
|
|
*,
|
2024-05-20 21:04:30 +00:00
|
|
|
|
2025-03-31 01:36:45 +00:00
|
|
|
bind_addrs: list[UnwrappedAddress]|None = None,
|
2024-04-14 21:49:18 +00:00
|
|
|
rpc_module_paths: list[str]|None = None,
|
2025-04-04 00:12:30 +00:00
|
|
|
enable_transports: list[str] = [_state._def_tpt_proto],
|
2024-04-14 21:49:18 +00:00
|
|
|
enable_modules: list[str]|None = None,
|
|
|
|
|
loglevel: str|None = None, # set log level per subactor
|
|
|
|
|
debug_mode: bool|None = None,
|
2020-07-26 04:35:41 +00:00
|
|
|
infect_asyncio: bool = False,
|
2026-04-06 06:30:00 +00:00
|
|
|
inherit_parent_main: bool = True,
|
2024-05-20 21:04:30 +00:00
|
|
|
|
2024-06-27 20:25:46 +00:00
|
|
|
# TODO: ideally we can rm this once we no longer have
|
|
|
|
|
# a `._ria_nursery` since the dependent APIs have been
|
|
|
|
|
# removed!
|
|
|
|
|
nursery: trio.Nursery|None = None,
|
2026-04-06 22:32:50 +00:00
|
|
|
proc_kwargs: dict[str, typing.Any] | None = None,
|
2024-06-27 20:25:46 +00:00
|
|
|
|
2018-08-20 02:13:13 +00:00
|
|
|
) -> Portal:
|
2021-12-09 22:51:36 +00:00
|
|
|
'''
|
|
|
|
|
Start a (daemon) actor: an process that has no designated
|
|
|
|
|
"main task" besides the runtime.
|
|
|
|
|
|
2026-04-10 15:29:34 +00:00
|
|
|
Pass ``inherit_parent_main=False`` to keep this child on its
|
|
|
|
|
own bootstrap module for the trio spawn backend instead of
|
|
|
|
|
applying the parent ``__main__`` re-exec fixup during startup.
|
|
|
|
|
This does not affect ``multiprocessing`` ``spawn`` or
|
|
|
|
|
``forkserver`` which reconstruct the parent's ``__main__`` as
|
|
|
|
|
part of their normal stdlib bootstrap.
|
2026-04-06 05:51:02 +00:00
|
|
|
|
2021-12-09 22:51:36 +00:00
|
|
|
'''
|
2024-04-18 19:17:50 +00:00
|
|
|
__runtimeframe__: int = 1 # noqa
|
|
|
|
|
loglevel: str = (
|
|
|
|
|
loglevel
|
|
|
|
|
or self._actor.loglevel
|
|
|
|
|
or get_loglevel()
|
|
|
|
|
)
|
2020-01-12 02:37:30 +00:00
|
|
|
|
2020-07-23 17:23:55 +00:00
|
|
|
# configure and pass runtime state
|
|
|
|
|
_rtv = _state._runtime_vars.copy()
|
|
|
|
|
_rtv['_is_root'] = False
|
2024-07-11 16:11:31 +00:00
|
|
|
_rtv['_is_infected_aio'] = infect_asyncio
|
2020-07-23 17:23:55 +00:00
|
|
|
|
2021-03-11 15:07:39 +00:00
|
|
|
# allow setting debug policy per actor
|
|
|
|
|
if debug_mode is not None:
|
|
|
|
|
_rtv['_debug_mode'] = debug_mode
|
2021-12-09 22:51:36 +00:00
|
|
|
self._at_least_one_child_in_debug = True
|
2021-03-11 15:07:39 +00:00
|
|
|
|
2026-04-06 22:32:50 +00:00
|
|
|
enable_modules = list(enable_modules or [])
|
|
|
|
|
proc_kwargs = dict(proc_kwargs or {})
|
2021-01-05 13:28:06 +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)
|
|
|
|
|
|
2020-01-20 16:10:51 +00:00
|
|
|
subactor = Actor(
|
2025-03-31 01:36:45 +00:00
|
|
|
name=name,
|
|
|
|
|
uuid=mk_uuid(),
|
|
|
|
|
|
2018-07-14 20:09:05 +00:00
|
|
|
# modules allowed to invoked funcs from
|
2021-01-05 13:28:06 +00:00
|
|
|
enable_modules=enable_modules,
|
2018-07-14 20:09:05 +00:00
|
|
|
loglevel=loglevel,
|
2026-04-06 06:30:00 +00:00
|
|
|
inherit_parent_main=inherit_parent_main,
|
2023-09-27 19:19:30 +00:00
|
|
|
|
|
|
|
|
# verbatim relay this actor's registrar addresses
|
2025-07-07 14:59:00 +00:00
|
|
|
registry_addrs=current_actor().registry_addrs,
|
2018-07-14 20:09:05 +00:00
|
|
|
)
|
2025-03-31 01:36:45 +00:00
|
|
|
parent_addr: UnwrappedAddress = self._actor.accept_addr
|
2019-11-26 14:23:37 +00:00
|
|
|
assert parent_addr
|
2020-01-20 16:10:51 +00:00
|
|
|
|
|
|
|
|
# start a task to spawn a process
|
|
|
|
|
# blocks until process has been started and a portal setup
|
2024-02-20 14:18:22 +00:00
|
|
|
nursery: trio.Nursery = nursery or self._da_nursery
|
2020-01-21 02:06:49 +00:00
|
|
|
|
|
|
|
|
# XXX: the type ignore is actually due to a `mypy` bug
|
|
|
|
|
return await nursery.start( # type: ignore
|
2020-06-28 17:10:02 +00:00
|
|
|
partial(
|
|
|
|
|
_spawn.new_proc,
|
|
|
|
|
name,
|
|
|
|
|
self,
|
|
|
|
|
subactor,
|
|
|
|
|
self.errors,
|
2023-09-27 19:19:30 +00:00
|
|
|
bind_addrs,
|
2020-06-28 17:10:02 +00:00
|
|
|
parent_addr,
|
2020-07-23 17:23:55 +00:00
|
|
|
_rtv, # run time vars
|
2020-07-26 04:35:41 +00:00
|
|
|
infect_asyncio=infect_asyncio,
|
2025-03-12 19:13:40 +00:00
|
|
|
proc_kwargs=proc_kwargs
|
2020-06-28 17:10:02 +00:00
|
|
|
)
|
2018-07-14 20:09:05 +00:00
|
|
|
)
|
2018-08-01 19:15:18 +00:00
|
|
|
|
2024-05-20 21:04:30 +00:00
|
|
|
# TODO: DEPRECATE THIS:
|
|
|
|
|
# -[ ] impl instead as a hilevel wrapper on
|
|
|
|
|
# top of a `@context` style invocation.
|
|
|
|
|
# |_ dynamic @context decoration on child side
|
|
|
|
|
# |_ implicit `Portal.open_context() as (ctx, first):`
|
|
|
|
|
# and `return first` on parent side.
|
2024-06-27 20:25:46 +00:00
|
|
|
# |_ mention how it's similar to `trio-parallel` API?
|
2024-05-20 21:04:30 +00:00
|
|
|
# -[ ] use @api_frame on the wrapper
|
2018-08-01 19:15:18 +00:00
|
|
|
async def run_in_actor(
|
|
|
|
|
self,
|
2021-11-07 21:45:00 +00:00
|
|
|
|
2018-08-20 02:13:13 +00:00
|
|
|
fn: typing.Callable,
|
2020-07-20 20:08:03 +00:00
|
|
|
*,
|
2021-11-07 21:45:00 +00:00
|
|
|
|
2023-09-27 19:19:30 +00:00
|
|
|
name: str | None = None,
|
2025-03-31 01:36:45 +00:00
|
|
|
bind_addrs: UnwrappedAddress|None = None,
|
2022-12-12 18:18:22 +00:00
|
|
|
rpc_module_paths: list[str] | None = None,
|
|
|
|
|
enable_modules: list[str] | None = None,
|
|
|
|
|
loglevel: str | None = None, # set log level per subactor
|
2020-07-26 04:35:41 +00:00
|
|
|
infect_asyncio: bool = False,
|
2026-04-06 06:30:00 +00:00
|
|
|
inherit_parent_main: bool = True,
|
2026-04-06 22:32:50 +00:00
|
|
|
proc_kwargs: dict[str, typing.Any] | None = None,
|
2021-11-07 21:45:00 +00:00
|
|
|
|
2018-08-01 19:15:18 +00:00
|
|
|
**kwargs, # explicit args to ``fn``
|
2021-11-07 21:45:00 +00:00
|
|
|
|
2018-08-20 02:13:13 +00:00
|
|
|
) -> Portal:
|
2024-02-21 18:21:28 +00:00
|
|
|
'''
|
|
|
|
|
Spawn a new actor, run a lone task, then terminate the actor and
|
2018-08-01 19:15:18 +00:00
|
|
|
return its result.
|
|
|
|
|
|
|
|
|
|
Actors spawned using this method are kept alive at nursery teardown
|
|
|
|
|
until the task spawned by executing ``fn`` completes at which point
|
|
|
|
|
the actor is terminated.
|
2024-02-21 18:21:28 +00:00
|
|
|
|
|
|
|
|
'''
|
2024-04-18 19:17:50 +00:00
|
|
|
__runtimeframe__: int = 1 # noqa
|
2024-02-21 18:21:28 +00:00
|
|
|
mod_path: str = fn.__module__
|
2020-12-21 14:09:55 +00:00
|
|
|
|
|
|
|
|
if name is None:
|
|
|
|
|
# use the explicit function name if not provided
|
|
|
|
|
name = fn.__name__
|
|
|
|
|
|
2026-04-06 22:32:50 +00:00
|
|
|
proc_kwargs = dict(proc_kwargs or {})
|
2024-05-20 21:04:30 +00:00
|
|
|
portal: Portal = await self.start_actor(
|
2018-08-01 19:15:18 +00:00
|
|
|
name,
|
2021-04-29 02:10:59 +00:00
|
|
|
enable_modules=[mod_path] + (
|
|
|
|
|
enable_modules or rpc_module_paths or []
|
|
|
|
|
),
|
2023-09-27 19:19:30 +00:00
|
|
|
bind_addrs=bind_addrs,
|
2019-03-19 01:32:08 +00:00
|
|
|
loglevel=loglevel,
|
2020-01-20 16:10:51 +00:00
|
|
|
# use the run_in_actor nursery
|
|
|
|
|
nursery=self._ria_nursery,
|
2020-07-26 04:35:41 +00:00
|
|
|
infect_asyncio=infect_asyncio,
|
2026-04-06 06:30:00 +00:00
|
|
|
inherit_parent_main=inherit_parent_main,
|
2025-03-12 19:13:40 +00:00
|
|
|
proc_kwargs=proc_kwargs
|
2018-08-01 19:15:18 +00:00
|
|
|
)
|
2021-04-28 15:39:26 +00:00
|
|
|
|
|
|
|
|
# XXX: don't allow stream funcs
|
|
|
|
|
if not (
|
|
|
|
|
inspect.iscoroutinefunction(fn) and
|
|
|
|
|
not getattr(fn, '_tractor_stream_function', False)
|
|
|
|
|
):
|
|
|
|
|
raise TypeError(f'{fn} must be an async function!')
|
|
|
|
|
|
2018-11-19 09:12:54 +00:00
|
|
|
# this marks the actor to be cancelled after its portal result
|
2020-01-20 18:39:11 +00:00
|
|
|
# is retreived, see logic in `open_nursery()` below.
|
2018-08-15 02:51:37 +00:00
|
|
|
self._cancel_after_result_on_exit.add(portal)
|
2018-08-01 19:15:18 +00:00
|
|
|
await portal._submit_for_result(
|
|
|
|
|
mod_path,
|
|
|
|
|
fn.__name__,
|
|
|
|
|
**kwargs
|
|
|
|
|
)
|
2018-07-14 20:09:05 +00:00
|
|
|
return portal
|
|
|
|
|
|
2024-05-20 21:04:30 +00:00
|
|
|
# @api_frame
|
2024-02-21 18:21:28 +00:00
|
|
|
async def cancel(
|
|
|
|
|
self,
|
|
|
|
|
hard_kill: bool = False,
|
|
|
|
|
|
|
|
|
|
) -> None:
|
2024-02-20 14:18:22 +00:00
|
|
|
'''
|
2024-06-27 20:25:46 +00:00
|
|
|
Cancel this actor-nursery by instructing each subactor's
|
|
|
|
|
runtime to cancel and wait for all underlying sub-processes
|
|
|
|
|
to terminate.
|
2018-07-14 20:09:05 +00:00
|
|
|
|
2024-06-27 20:25:46 +00:00
|
|
|
If `hard_kill` is set then kill the processes directly using
|
|
|
|
|
the spawning-backend's API/OS-machinery without any attempt
|
|
|
|
|
at (graceful) `trio`-style cancellation using our
|
|
|
|
|
`Actor.cancel()`.
|
2024-02-20 14:18:22 +00:00
|
|
|
|
|
|
|
|
'''
|
2024-04-18 19:17:50 +00:00
|
|
|
__runtimeframe__: int = 1 # noqa
|
2025-07-15 23:29:38 +00:00
|
|
|
self._cancel_called = True
|
2020-09-28 17:49:45 +00:00
|
|
|
|
2024-03-01 20:44:01 +00:00
|
|
|
# TODO: impl a repr for spawn more compact
|
|
|
|
|
# then `._children`..
|
|
|
|
|
children: dict = self._children
|
|
|
|
|
child_count: int = len(children)
|
|
|
|
|
msg: str = f'Cancelling actor nursery with {child_count} children\n'
|
2025-04-11 20:55:03 +00:00
|
|
|
|
|
|
|
|
server: IPCServer = self._actor.ipc_server
|
|
|
|
|
|
2019-11-22 22:11:48 +00:00
|
|
|
with trio.move_on_after(3) as cs:
|
2025-06-16 17:23:54 +00:00
|
|
|
async with (
|
|
|
|
|
collapse_eg(),
|
|
|
|
|
trio.open_nursery() as tn,
|
|
|
|
|
):
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2024-02-20 14:18:22 +00:00
|
|
|
subactor: Actor
|
|
|
|
|
proc: trio.Process
|
|
|
|
|
portal: Portal
|
|
|
|
|
for (
|
|
|
|
|
subactor,
|
|
|
|
|
proc,
|
|
|
|
|
portal,
|
2024-03-01 20:44:01 +00:00
|
|
|
) in children.values():
|
2021-06-29 19:15:32 +00:00
|
|
|
|
|
|
|
|
# TODO: are we ever even going to use this or
|
|
|
|
|
# is the spawning backend responsible for such
|
|
|
|
|
# things? I'm thinking latter.
|
2018-08-01 19:15:18 +00:00
|
|
|
if hard_kill:
|
2020-09-28 17:49:45 +00:00
|
|
|
proc.terminate()
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2018-08-01 19:15:18 +00:00
|
|
|
else:
|
|
|
|
|
if portal is None: # actor hasn't fully spawned yet
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
event: trio.Event = server._peer_connected[
|
|
|
|
|
subactor.aid.uid
|
|
|
|
|
]
|
2018-09-10 19:19:49 +00:00
|
|
|
log.warning(
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
f"{subactor.aid.uid} never 't finished spawning?"
|
2024-03-01 20:44:01 +00:00
|
|
|
)
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2018-08-01 19:15:18 +00:00
|
|
|
await event.wait()
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2018-08-01 19:15:18 +00:00
|
|
|
# channel/portal should now be up
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
_, _, portal = children[subactor.aid.uid]
|
2019-11-22 22:11:48 +00:00
|
|
|
|
|
|
|
|
# XXX should be impossible to get here
|
|
|
|
|
# unless method was called from within
|
|
|
|
|
# shielded cancel scope.
|
2018-08-01 19:15:18 +00:00
|
|
|
if portal is None:
|
2018-11-19 09:12:54 +00:00
|
|
|
# cancelled while waiting on the event
|
|
|
|
|
# to arrive
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
chan = server._peers[subactor.aid.uid][-1]
|
2018-08-01 19:15:18 +00:00
|
|
|
if chan:
|
|
|
|
|
portal = Portal(chan)
|
|
|
|
|
else: # there's no other choice left
|
2020-09-28 17:49:45 +00:00
|
|
|
proc.terminate()
|
2018-08-01 19:15:18 +00:00
|
|
|
|
2026-05-07 22:01:59 +00:00
|
|
|
# spawn per-child cancel tasks; the helper
|
|
|
|
|
# escalates to hard-kill on
|
|
|
|
|
# `ActorTooSlowError` rather than silently
|
|
|
|
|
# swallowing the cancel-ack timeout, EXCEPT
|
|
|
|
|
# when this nursery has any debug-eligible
|
|
|
|
|
# child (in which case we keep legacy
|
|
|
|
|
# fire-and-forget semantics to avoid
|
|
|
|
|
# clobbering an active REPL).
|
2018-08-31 21:16:24 +00:00
|
|
|
assert portal
|
2021-12-02 03:05:23 +00:00
|
|
|
if portal.channel.connected():
|
2026-05-07 22:01:59 +00:00
|
|
|
tn.start_soon(
|
|
|
|
|
_try_cancel_then_kill,
|
|
|
|
|
portal,
|
|
|
|
|
proc,
|
|
|
|
|
subactor,
|
|
|
|
|
self._at_least_one_child_in_debug,
|
|
|
|
|
)
|
2018-07-14 20:09:05 +00:00
|
|
|
|
2024-03-01 20:44:01 +00:00
|
|
|
log.cancel(msg)
|
2019-11-22 22:11:48 +00:00
|
|
|
# if we cancelled the cancel (we hung cancelling remote actors)
|
|
|
|
|
# then hard kill all sub-processes
|
|
|
|
|
if cs.cancelled_caught:
|
2020-09-28 17:49:45 +00:00
|
|
|
log.error(
|
2024-03-01 20:44:01 +00:00
|
|
|
f'Failed to cancel {self}?\n'
|
|
|
|
|
'Hard killing underlying subprocess tree!\n'
|
2024-02-20 14:18:22 +00:00
|
|
|
)
|
|
|
|
|
subactor: Actor
|
|
|
|
|
proc: trio.Process
|
|
|
|
|
portal: Portal
|
|
|
|
|
for (
|
|
|
|
|
subactor,
|
|
|
|
|
proc,
|
|
|
|
|
portal,
|
2024-03-01 20:44:01 +00:00
|
|
|
) in children.values():
|
2020-09-28 17:49:45 +00:00
|
|
|
log.warning(f"Hard killing process {proc}")
|
|
|
|
|
proc.terminate()
|
2025-07-15 23:29:38 +00:00
|
|
|
else:
|
|
|
|
|
self._cancelled_caught
|
2019-11-22 22:11:48 +00:00
|
|
|
|
2018-11-19 09:12:54 +00:00
|
|
|
# mark ourselves as having (tried to have) cancelled all subactors
|
2020-01-20 18:39:11 +00:00
|
|
|
self._join_procs.set()
|
2018-07-14 20:09:05 +00:00
|
|
|
|
|
|
|
|
|
2022-10-09 17:12:50 +00:00
|
|
|
@acm
|
2021-02-24 17:59:43 +00:00
|
|
|
async def _open_and_supervise_one_cancels_all_nursery(
|
|
|
|
|
actor: Actor,
|
2025-09-08 22:15:00 +00:00
|
|
|
hide_tb: bool = True,
|
2022-10-14 20:17:22 +00:00
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
) -> typing.AsyncGenerator[ActorNursery, None]:
|
2024-05-20 21:04:30 +00:00
|
|
|
|
|
|
|
|
# normally don't need to show user by default
|
2025-09-08 22:15:00 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
2024-05-20 21:04:30 +00:00
|
|
|
|
|
|
|
|
outer_err: BaseException|None = None
|
|
|
|
|
inner_err: BaseException|None = None
|
2022-10-14 20:17:22 +00:00
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
# the collection of errors retreived from spawned sub-actors
|
2022-10-12 21:40:08 +00:00
|
|
|
errors: dict[tuple[str, str], BaseException] = {}
|
2021-02-24 17:59:43 +00:00
|
|
|
|
|
|
|
|
# This is the outermost level "deamon actor" nursery. It is awaited
|
|
|
|
|
# **after** the below inner "run in actor nursery". This allows for
|
|
|
|
|
# handling errors that are generated by the inner nursery in
|
|
|
|
|
# a supervisor strategy **before** blocking indefinitely to wait for
|
|
|
|
|
# actors spawned in "daemon mode" (aka started using
|
2024-05-20 21:04:30 +00:00
|
|
|
# `ActorNursery.start_actor()`).
|
2021-02-24 17:59:43 +00:00
|
|
|
|
|
|
|
|
# errors from this daemon actor nursery bubble up to caller
|
2025-06-16 17:23:54 +00:00
|
|
|
async with (
|
|
|
|
|
collapse_eg(),
|
|
|
|
|
trio.open_nursery() as da_nursery,
|
|
|
|
|
):
|
2021-02-24 17:59:43 +00:00
|
|
|
try:
|
|
|
|
|
# This is the inner level "run in actor" nursery. It is
|
|
|
|
|
# awaited first since actors spawned in this way (using
|
2025-02-26 18:04:37 +00:00
|
|
|
# `ActorNusery.run_in_actor()`) are expected to only
|
2021-02-24 17:59:43 +00:00
|
|
|
# return a single result and then complete (i.e. be canclled
|
|
|
|
|
# gracefully). Errors collected from these actors are
|
|
|
|
|
# immediately raised for handling by a supervisor strategy.
|
|
|
|
|
# As such if the strategy propagates any error(s) upwards
|
|
|
|
|
# the above "daemon actor" nursery will be notified.
|
2025-06-16 17:23:54 +00:00
|
|
|
async with (
|
|
|
|
|
collapse_eg(),
|
|
|
|
|
trio.open_nursery() as ria_nursery,
|
|
|
|
|
):
|
2024-02-21 18:21:28 +00:00
|
|
|
an = ActorNursery(
|
2021-02-24 17:59:43 +00:00
|
|
|
actor,
|
|
|
|
|
ria_nursery,
|
|
|
|
|
da_nursery,
|
|
|
|
|
errors
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
# spawning of actors happens in the caller's scope
|
|
|
|
|
# after we yield upwards
|
2024-02-21 18:21:28 +00:00
|
|
|
yield an
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2022-10-14 20:17:22 +00:00
|
|
|
# When we didn't error in the caller's scope,
|
|
|
|
|
# signal all process-monitor-tasks to conduct
|
|
|
|
|
# the "hard join phase".
|
2021-06-29 19:15:32 +00:00
|
|
|
log.runtime(
|
2024-02-21 18:21:28 +00:00
|
|
|
'Waiting on subactors to complete:\n'
|
2025-07-07 14:59:00 +00:00
|
|
|
f'>}} {len(an._children)}\n'
|
2021-02-24 17:59:43 +00:00
|
|
|
)
|
2024-02-21 18:21:28 +00:00
|
|
|
an._join_procs.set()
|
2021-07-05 17:37:09 +00:00
|
|
|
|
2024-05-20 21:04:30 +00:00
|
|
|
except BaseException as _inner_err:
|
|
|
|
|
inner_err = _inner_err
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
errors[actor.aid.uid] = inner_err
|
2022-10-14 20:17:22 +00:00
|
|
|
|
2021-06-29 19:15:32 +00:00
|
|
|
# If we error in the root but the debugger is
|
|
|
|
|
# engaged we don't want to prematurely kill (and
|
|
|
|
|
# thus clobber access to) the local tty since it
|
|
|
|
|
# will make the pdb repl unusable.
|
|
|
|
|
# Instead try to wait for pdb to be released before
|
|
|
|
|
# tearing down.
|
2025-07-07 14:59:00 +00:00
|
|
|
await debug.maybe_wait_for_debugger(
|
2024-02-21 18:21:28 +00:00
|
|
|
child_in_debug=an._at_least_one_child_in_debug
|
2021-12-09 22:51:36 +00:00
|
|
|
)
|
2021-06-10 18:02:12 +00:00
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
# if the caller's scope errored then we activate our
|
|
|
|
|
# one-cancels-all supervisor strategy (don't
|
|
|
|
|
# worry more are coming).
|
2024-02-21 18:21:28 +00:00
|
|
|
an._join_procs.set()
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2024-03-01 20:44:01 +00:00
|
|
|
# XXX NOTE XXX: hypothetically an error could
|
|
|
|
|
# be raised and then a cancel signal shows up
|
2022-10-14 20:17:22 +00:00
|
|
|
# slightly after in which case the `else:`
|
|
|
|
|
# block here might not complete? For now,
|
|
|
|
|
# shield both.
|
|
|
|
|
with trio.CancelScope(shield=True):
|
2024-03-01 20:44:01 +00:00
|
|
|
etype: type = type(inner_err)
|
2022-10-14 20:17:22 +00:00
|
|
|
if etype in (
|
|
|
|
|
trio.Cancelled,
|
2024-03-01 20:44:01 +00:00
|
|
|
KeyboardInterrupt,
|
2022-10-14 20:17:22 +00:00
|
|
|
) or (
|
|
|
|
|
is_multi_cancelled(inner_err)
|
|
|
|
|
):
|
|
|
|
|
log.cancel(
|
2024-03-01 20:44:01 +00:00
|
|
|
f'Actor-nursery cancelled by {etype}\n\n'
|
|
|
|
|
|
Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.
This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)
Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
and `portal.actor.uid` refs to `.aid.uid`; use
`.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
log msgs and cancel-scope handling; fix `str(err)` typo
in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
cache lookups, RPC `Start` msg, registration and
cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
`.aid.uid` for child-proc tracking, IPC peer waiting,
debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
`current_actor()` error msg.
Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
`.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
conversion.
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-08 19:27:48 +00:00
|
|
|
f'{current_actor().aid.uid}\n'
|
2024-03-01 20:44:01 +00:00
|
|
|
f' |_{an}\n\n'
|
|
|
|
|
|
|
|
|
|
# TODO: show tb str?
|
|
|
|
|
# f'{tb_str}'
|
|
|
|
|
)
|
|
|
|
|
elif etype in {
|
|
|
|
|
ContextCancelled,
|
|
|
|
|
}:
|
|
|
|
|
log.cancel(
|
2025-02-26 18:04:37 +00:00
|
|
|
'Actor-nursery caught remote cancellation\n'
|
|
|
|
|
'\n'
|
2024-03-01 20:44:01 +00:00
|
|
|
f'{inner_err.tb_str}'
|
|
|
|
|
)
|
2022-10-14 20:17:22 +00:00
|
|
|
else:
|
|
|
|
|
log.exception(
|
2024-03-01 20:44:01 +00:00
|
|
|
'Nursery errored with:\n'
|
2024-02-20 14:18:22 +00:00
|
|
|
|
|
|
|
|
# TODO: same thing as in
|
|
|
|
|
# `._invoke()` to compute how to
|
|
|
|
|
# place this div-line in the
|
|
|
|
|
# middle of the above msg
|
|
|
|
|
# content..
|
|
|
|
|
# -[ ] prolly helper-func it too
|
|
|
|
|
# in our `.log` module..
|
|
|
|
|
# '------ - ------'
|
|
|
|
|
)
|
2022-10-14 20:17:22 +00:00
|
|
|
|
|
|
|
|
# cancel all subactors
|
2024-02-21 18:21:28 +00:00
|
|
|
await an.cancel()
|
2021-02-24 17:59:43 +00:00
|
|
|
|
2022-10-14 20:17:22 +00:00
|
|
|
# ria_nursery scope end
|
2021-02-24 17:59:43 +00:00
|
|
|
|
2022-10-14 20:17:22 +00:00
|
|
|
# TODO: this is the handler around the ``.run_in_actor()``
|
|
|
|
|
# nursery. Ideally we can drop this entirely in the future as
|
|
|
|
|
# the whole ``.run_in_actor()`` API should be built "on top of"
|
|
|
|
|
# this lower level spawn-request-cancel "daemon actor" API where
|
|
|
|
|
# a local in-actor task nursery is used with one-to-one task
|
|
|
|
|
# + `await Portal.run()` calls and the results/errors are
|
|
|
|
|
# handled directly (inline) and errors by the local nursery.
|
2021-06-29 19:15:32 +00:00
|
|
|
except (
|
|
|
|
|
Exception,
|
2022-10-09 17:12:50 +00:00
|
|
|
BaseExceptionGroup,
|
2021-06-29 19:15:32 +00:00
|
|
|
trio.Cancelled
|
2024-05-20 21:04:30 +00:00
|
|
|
) as _outer_err:
|
|
|
|
|
outer_err = _outer_err
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2024-05-20 21:04:30 +00:00
|
|
|
an._scope_error = outer_err or inner_err
|
2021-06-29 19:15:32 +00:00
|
|
|
|
2021-12-10 16:54:27 +00:00
|
|
|
# XXX: yet another guard before allowing the cancel
|
|
|
|
|
# sequence in case a (single) child is in debug.
|
2025-07-07 14:59:00 +00:00
|
|
|
await debug.maybe_wait_for_debugger(
|
2024-02-21 18:21:28 +00:00
|
|
|
child_in_debug=an._at_least_one_child_in_debug
|
2021-12-09 22:51:36 +00:00
|
|
|
)
|
2021-12-10 16:54:27 +00:00
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
# If actor-local error was raised while waiting on
|
|
|
|
|
# ".run_in_actor()" actors then we also want to cancel all
|
|
|
|
|
# remaining sub-actors (due to our lone strategy:
|
|
|
|
|
# one-cancels-all).
|
2024-02-21 18:21:28 +00:00
|
|
|
if an._children:
|
2024-03-01 20:44:01 +00:00
|
|
|
log.cancel(
|
|
|
|
|
'Actor-nursery cancelling due error type:\n'
|
2024-05-20 21:04:30 +00:00
|
|
|
f'{outer_err}\n'
|
2024-03-01 20:44:01 +00:00
|
|
|
)
|
2021-02-24 17:59:43 +00:00
|
|
|
with trio.CancelScope(shield=True):
|
2024-02-21 18:21:28 +00:00
|
|
|
await an.cancel()
|
2021-02-24 17:59:43 +00:00
|
|
|
raise
|
2024-03-01 20:44:01 +00:00
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
finally:
|
|
|
|
|
# No errors were raised while awaiting ".run_in_actor()"
|
|
|
|
|
# actors but those actors may have returned remote errors as
|
|
|
|
|
# results (meaning they errored remotely and have relayed
|
|
|
|
|
# those errors back to this parent actor). The errors are
|
|
|
|
|
# collected in ``errors`` so cancel all actors, summarize
|
|
|
|
|
# all errors and re-raise.
|
|
|
|
|
if errors:
|
2024-02-21 18:21:28 +00:00
|
|
|
if an._children:
|
2021-02-24 17:59:43 +00:00
|
|
|
with trio.CancelScope(shield=True):
|
2024-02-21 18:21:28 +00:00
|
|
|
await an.cancel()
|
2021-02-24 17:59:43 +00:00
|
|
|
|
2022-10-09 17:12:50 +00:00
|
|
|
# use `BaseExceptionGroup` as needed
|
2021-02-24 17:59:43 +00:00
|
|
|
if len(errors) > 1:
|
2022-10-09 17:12:50 +00:00
|
|
|
raise BaseExceptionGroup(
|
|
|
|
|
'tractor.ActorNursery errored with',
|
|
|
|
|
tuple(errors.values()),
|
|
|
|
|
)
|
2021-02-24 17:59:43 +00:00
|
|
|
else:
|
|
|
|
|
raise list(errors.values())[0]
|
|
|
|
|
|
2024-05-20 21:04:30 +00:00
|
|
|
# show frame on any (likely) internal error
|
|
|
|
|
if (
|
2026-03-13 20:48:58 +00:00
|
|
|
not an.cancel_called
|
2024-05-20 21:04:30 +00:00
|
|
|
and an._scope_error
|
|
|
|
|
):
|
|
|
|
|
__tracebackhide__: bool = False
|
|
|
|
|
|
2022-10-14 20:17:22 +00:00
|
|
|
# da_nursery scope end - nursery checkpoint
|
|
|
|
|
# final exit
|
2021-02-24 17:59:43 +00:00
|
|
|
|
|
|
|
|
|
2025-07-07 14:59:00 +00:00
|
|
|
_shutdown_msg: str = (
|
|
|
|
|
'Actor-runtime-shutdown'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2022-10-09 17:12:50 +00:00
|
|
|
@acm
|
2024-05-09 19:20:03 +00:00
|
|
|
# @api_frame
|
2021-01-03 02:35:47 +00:00
|
|
|
async def open_nursery(
|
2025-06-17 16:31:36 +00:00
|
|
|
*, # named params only!
|
2025-03-03 17:18:10 +00:00
|
|
|
hide_tb: bool = True,
|
2021-01-03 02:35:47 +00:00
|
|
|
**kwargs,
|
2025-02-26 18:04:37 +00:00
|
|
|
# ^TODO, paramspec for `open_root_actor()`
|
2021-11-07 21:45:00 +00:00
|
|
|
|
2021-01-03 02:35:47 +00:00
|
|
|
) -> typing.AsyncGenerator[ActorNursery, None]:
|
2021-11-07 21:45:00 +00:00
|
|
|
'''
|
|
|
|
|
Create and yield a new ``ActorNursery`` to be used for spawning
|
2020-01-31 14:50:25 +00:00
|
|
|
structured concurrent subactors.
|
2020-01-12 02:37:30 +00:00
|
|
|
|
2020-01-31 14:50:25 +00:00
|
|
|
When an actor is spawned a new trio task is started which
|
|
|
|
|
invokes one of the process spawning backends to create and start
|
|
|
|
|
a new subprocess. These tasks are started by one of two nurseries
|
|
|
|
|
detailed below. The reason for spawning processes from within
|
|
|
|
|
a new task is because ``trio_run_in_process`` itself creates a new
|
|
|
|
|
internal nursery and the same task that opens a nursery **must**
|
|
|
|
|
close it. It turns out this approach is probably more correct
|
|
|
|
|
anyway since it is more clear from the following nested nurseries
|
|
|
|
|
which cancellation scopes correspond to each spawned subactor set.
|
2021-11-07 21:45:00 +00:00
|
|
|
|
|
|
|
|
'''
|
2025-02-26 18:04:37 +00:00
|
|
|
__tracebackhide__: bool = hide_tb
|
2024-03-01 20:44:01 +00:00
|
|
|
implicit_runtime: bool = False
|
2024-03-12 12:56:17 +00:00
|
|
|
actor: Actor = current_actor(err_on_no_runtime=False)
|
|
|
|
|
an: ActorNursery|None = None
|
2021-02-24 18:07:22 +00:00
|
|
|
try:
|
2024-03-08 19:11:17 +00:00
|
|
|
if (
|
|
|
|
|
actor is None
|
|
|
|
|
and is_main_process()
|
|
|
|
|
):
|
2021-05-10 11:23:39 +00:00
|
|
|
# if we are the parent process start the
|
|
|
|
|
# actor runtime implicitly
|
2021-02-24 18:07:22 +00:00
|
|
|
log.info("Starting actor runtime!")
|
2021-01-03 02:35:47 +00:00
|
|
|
|
2021-02-24 18:07:22 +00:00
|
|
|
# mark us for teardown on exit
|
2024-03-01 20:44:01 +00:00
|
|
|
implicit_runtime: bool = True
|
2021-01-03 02:35:47 +00:00
|
|
|
|
2025-02-26 18:04:37 +00:00
|
|
|
async with open_root_actor(
|
|
|
|
|
hide_tb=hide_tb,
|
|
|
|
|
**kwargs,
|
|
|
|
|
) as actor:
|
2021-02-24 18:07:22 +00:00
|
|
|
assert actor is current_actor()
|
|
|
|
|
|
2021-12-02 03:05:23 +00:00
|
|
|
try:
|
|
|
|
|
async with _open_and_supervise_one_cancels_all_nursery(
|
|
|
|
|
actor
|
2024-02-21 18:21:28 +00:00
|
|
|
) as an:
|
2024-03-01 20:44:01 +00:00
|
|
|
|
|
|
|
|
# NOTE: mark this nursery as having
|
|
|
|
|
# implicitly started the root actor so
|
|
|
|
|
# that `._runtime` machinery can avoid
|
|
|
|
|
# certain teardown synchronization
|
|
|
|
|
# blocking/waits and any associated (warn)
|
|
|
|
|
# logging when it's known that this
|
|
|
|
|
# nursery shouldn't be exited before the
|
|
|
|
|
# root actor is.
|
|
|
|
|
an._implicit_runtime_started = True
|
2024-02-21 18:21:28 +00:00
|
|
|
yield an
|
2021-12-02 03:05:23 +00:00
|
|
|
finally:
|
2024-03-01 20:44:01 +00:00
|
|
|
# XXX: this event will be set after the root actor
|
|
|
|
|
# runtime is already torn down, so we want to
|
|
|
|
|
# avoid any blocking on it.
|
2024-02-21 18:21:28 +00:00
|
|
|
an.exited.set()
|
2021-12-02 03:05:23 +00:00
|
|
|
|
|
|
|
|
else: # sub-nursery case
|
|
|
|
|
|
|
|
|
|
try:
|
2021-06-10 18:02:12 +00:00
|
|
|
async with _open_and_supervise_one_cancels_all_nursery(
|
|
|
|
|
actor
|
2024-02-21 18:21:28 +00:00
|
|
|
) as an:
|
|
|
|
|
yield an
|
2021-12-02 03:05:23 +00:00
|
|
|
finally:
|
2024-02-21 18:21:28 +00:00
|
|
|
an.exited.set()
|
2020-01-31 14:50:25 +00:00
|
|
|
|
2021-01-03 02:35:47 +00:00
|
|
|
finally:
|
2024-05-20 21:04:30 +00:00
|
|
|
# show frame on any internal runtime-scope error
|
|
|
|
|
if (
|
|
|
|
|
an
|
2025-02-26 18:04:37 +00:00
|
|
|
and
|
2026-03-13 20:48:58 +00:00
|
|
|
not an.cancel_called
|
2025-02-26 18:04:37 +00:00
|
|
|
and
|
|
|
|
|
an._scope_error
|
2024-05-20 21:04:30 +00:00
|
|
|
):
|
|
|
|
|
__tracebackhide__: bool = False
|
|
|
|
|
|
2025-07-07 14:59:00 +00:00
|
|
|
|
|
|
|
|
op_nested_an_repr: str = _pformat.nest_from_op(
|
|
|
|
|
input_op=')>',
|
|
|
|
|
text=f'{an}',
|
|
|
|
|
# nest_prefix='|_',
|
|
|
|
|
nest_indent=1, # under >
|
|
|
|
|
)
|
|
|
|
|
an_msg: str = (
|
|
|
|
|
f'Actor-nursery exited\n'
|
|
|
|
|
f'{op_nested_an_repr}\n'
|
2024-03-01 20:44:01 +00:00
|
|
|
)
|
2025-07-07 14:59:00 +00:00
|
|
|
# keep noise low during std operation.
|
|
|
|
|
log.runtime(an_msg)
|
2020-01-31 14:50:25 +00:00
|
|
|
|
2021-01-03 02:35:47 +00:00
|
|
|
if implicit_runtime:
|
2024-06-27 20:25:46 +00:00
|
|
|
# shutdown runtime if it was started and report noisly
|
|
|
|
|
# that we're did so.
|
2025-07-07 14:59:00 +00:00
|
|
|
msg: str = (
|
|
|
|
|
'\n'
|
|
|
|
|
'\n'
|
|
|
|
|
f'{_shutdown_msg} )>\n'
|
|
|
|
|
)
|
2024-06-27 20:25:46 +00:00
|
|
|
log.info(msg)
|