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
|
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,
|
|
|
|
|
)
|
2026-08-25 01:56:00 +00:00
|
|
|
from ..msg import Aid
|
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
|
2026-08-18 05:58:45 +00:00
|
|
|
we escalate via `proc.kill()` per SC-discipline:
|
2026-05-07 22:01:59 +00:00
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-08-18 05:58:45 +00:00
|
|
|
# XXX, delay hard-kill escalation while any debugger guard
|
|
|
|
|
# below is active. Killing the sub immediately would tear down
|
|
|
|
|
# its tree and clobber an actor proxying a REPL session:
|
2026-05-07 22:01:59 +00:00
|
|
|
#
|
|
|
|
|
# - `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
|
2026-08-18 05:58:45 +00:00
|
|
|
# child started with an explicit `debug_mode=True` arg
|
2026-05-07 22:01:59 +00:00
|
|
|
# (`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.
|
2026-08-24 23:38:22 +00:00
|
|
|
def child_in_debug() -> bool:
|
|
|
|
|
'''
|
|
|
|
|
Sample child/tree debugger protection state.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
return (
|
|
|
|
|
debug_mode_active
|
|
|
|
|
or
|
|
|
|
|
debug.Lock.ctx_in_debug is not None
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-18 05:58:45 +00:00
|
|
|
debug_protected: bool = (
|
2026-08-24 23:38:22 +00:00
|
|
|
child_in_debug()
|
2026-05-07 22:01:59 +00:00
|
|
|
or
|
|
|
|
|
_state._runtime_vars.get('_debug_mode', False)
|
2026-08-18 05:58:45 +00:00
|
|
|
)
|
2026-05-07 22:01:59 +00:00
|
|
|
|
|
|
|
|
try:
|
2026-08-18 05:58:45 +00:00
|
|
|
cancelled: bool = await portal.cancel_actor(
|
|
|
|
|
raise_on_timeout=not debug_protected,
|
|
|
|
|
)
|
|
|
|
|
if not cancelled:
|
|
|
|
|
if debug_protected:
|
|
|
|
|
await debug.maybe_wait_for_debugger(
|
2026-08-24 23:38:22 +00:00
|
|
|
# Re-sample after the cancel-RPC checkpoint.
|
|
|
|
|
child_in_debug=child_in_debug(),
|
2026-08-18 05:58:45 +00:00
|
|
|
header_msg=(
|
|
|
|
|
'Delaying subproc hard-reap while '
|
|
|
|
|
'debugger locked..\n'
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
peer_id: str = portal.channel.aid.reprol()
|
|
|
|
|
raise ActorTooSlowError(
|
|
|
|
|
f'Peer {peer_id} disconnected before '
|
|
|
|
|
f'acknowledging its `Actor.cancel()` RPC'
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-07 22:01:59 +00:00
|
|
|
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'
|
2026-08-18 05:58:45 +00:00
|
|
|
f'-> escalating to `proc.kill()` (hard-reap)\n'
|
2026-05-07 22:01:59 +00:00
|
|
|
)
|
2026-06-17 23:46:04 +00:00
|
|
|
# XXX, the `subint` backend stores an `int` interp-id in the
|
2026-08-18 05:58:45 +00:00
|
|
|
# `proc` slot (not a `Process`), so it has no `.kill()`.
|
2026-06-17 23:46:04 +00:00
|
|
|
# Guard here so a cancel-ack timeout doesn't `AttributeError`
|
|
|
|
|
# once that backend lands; its hard-kill path is a TODO.
|
2026-08-18 05:58:45 +00:00
|
|
|
if hasattr(proc, 'kill'):
|
|
|
|
|
if proc.poll() is None:
|
|
|
|
|
proc.kill()
|
2026-06-17 23:46:04 +00:00
|
|
|
else:
|
|
|
|
|
log.error(
|
|
|
|
|
f'Cannot hard-kill sub-actor — backend proc-handle '
|
|
|
|
|
f'{proc!r} ({type(proc).__name__!r}) has no '
|
2026-08-18 05:58:45 +00:00
|
|
|
f'`.kill()`!\n'
|
2026-06-17 23:46:04 +00:00
|
|
|
f' uid: {subactor.aid.reprol()!r}\n'
|
|
|
|
|
f'TODO: per-backend cancel-escalation.\n'
|
|
|
|
|
)
|
2026-05-07 22:01:59 +00:00
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
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()
|
2026-08-18 05:58:45 +00:00
|
|
|
self._child_reap_requests: dict[
|
2026-08-25 01:56:00 +00:00
|
|
|
Aid,
|
2026-08-18 05:58:45 +00:00
|
|
|
trio.Event,
|
|
|
|
|
] = {}
|
|
|
|
|
self._child_reaped: dict[
|
2026-08-25 01:56:00 +00:00
|
|
|
Aid,
|
2026-08-18 05:58:45 +00:00
|
|
|
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
|
|
|
|
|
|
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,
|
Fix informal-RST in docstrings for clean autodoc
The new `api/` reference pages surfaced 22 docutils warnings from
informal reST in public docstrings; fix the markup so the docs
build is warning-free (24 -> 0), clearing the path to a future
`-W`/`nitpicky` flip in CI.
Deats (docstring content only; no code/signature changes),
- give bullet lists a blank line + base-column indent (`Context`,
`Context.cancel_called`/`.cancelled_caught`/ `.outcome`,
`ActorNursery.cancel_called`, `query_actor`,
`open_crash_handler`),
- demote the under-short `Behaviour:` underline in `Context.cancel`
to a `**bold**` label,
- close an unbalanced backtick in the `wait_for_actor` summary and
use the `` `role`\ s `` escaped-plural idiom where a role was
pluralized (`gather_contexts`, `mk_pdb`, `MsgCodec`, msg `Error`,
`open_context_from_portal`),
- make the `|_` method-tree in `ContextCancelled.canceller` a
literal block (the bare `|` was read as a substitution ref),
- same blank-line fix for the `#318` entry in `NEWS.rst`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-06-25 23:20:30 +00:00
|
|
|
|
2025-07-15 23:29:38 +00:00
|
|
|
- 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
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-18 05:58:45 +00:00
|
|
|
def _register_child_reap(
|
|
|
|
|
self,
|
2026-08-25 01:56:00 +00:00
|
|
|
aid: Aid,
|
2026-08-18 05:58:45 +00:00
|
|
|
) -> tuple[trio.Event, trio.Event]:
|
|
|
|
|
'''
|
|
|
|
|
Register a child monitor's process-reap events.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
reap_request = trio.Event()
|
|
|
|
|
reaped = trio.Event()
|
2026-08-25 01:56:00 +00:00
|
|
|
self._child_reap_requests[aid] = reap_request
|
|
|
|
|
self._child_reaped[aid] = reaped
|
2026-08-18 05:58:45 +00:00
|
|
|
if self._join_procs.is_set():
|
|
|
|
|
reap_request.set()
|
|
|
|
|
return reap_request, reaped
|
|
|
|
|
|
2026-08-21 05:20:08 +00:00
|
|
|
def _register_child(
|
|
|
|
|
self,
|
|
|
|
|
subactor: Actor,
|
|
|
|
|
proc: 'ProcessType',
|
|
|
|
|
portal: Portal|None,
|
|
|
|
|
) -> tuple[trio.Event, trio.Event, bool]:
|
|
|
|
|
'''
|
|
|
|
|
Atomically publish one child and its reap coordination.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-08-25 01:56:00 +00:00
|
|
|
aid: Aid = subactor.aid
|
|
|
|
|
uid: tuple[str, str] = aid.uid
|
2026-08-21 05:20:08 +00:00
|
|
|
self._children[uid] = (
|
|
|
|
|
subactor,
|
|
|
|
|
proc,
|
|
|
|
|
portal,
|
|
|
|
|
)
|
2026-08-25 01:56:00 +00:00
|
|
|
reap_request, reaped = self._register_child_reap(aid)
|
2026-08-21 05:20:08 +00:00
|
|
|
return (
|
|
|
|
|
reap_request,
|
|
|
|
|
reaped,
|
|
|
|
|
self._cancel_called,
|
|
|
|
|
)
|
|
|
|
|
|
2026-08-18 05:58:45 +00:00
|
|
|
def _request_reap_all(self) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Release every child monitor into its process-join phase.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
self._join_procs.set()
|
|
|
|
|
for reap_request in tuple(
|
|
|
|
|
self._child_reap_requests.values()
|
|
|
|
|
):
|
|
|
|
|
reap_request.set()
|
|
|
|
|
|
|
|
|
|
def _mark_child_reaped(
|
|
|
|
|
self,
|
2026-08-25 01:56:00 +00:00
|
|
|
aid: Aid,
|
2026-08-18 05:58:45 +00:00
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Publish completed child-process teardown to its waiter.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-08-25 01:56:00 +00:00
|
|
|
uid: tuple[str, str] = aid.uid
|
2026-08-18 05:58:45 +00:00
|
|
|
self._children.pop(uid, None)
|
2026-08-24 22:50:59 +00:00
|
|
|
reap_request: trio.Event|None = (
|
2026-08-25 01:56:00 +00:00
|
|
|
self._child_reap_requests.pop(aid, None)
|
2026-08-24 22:50:59 +00:00
|
|
|
)
|
2026-08-18 05:58:45 +00:00
|
|
|
reaped: trio.Event|None = self._child_reaped.pop(
|
2026-08-25 01:56:00 +00:00
|
|
|
aid,
|
2026-08-18 05:58:45 +00:00
|
|
|
None,
|
|
|
|
|
)
|
2026-08-24 22:50:59 +00:00
|
|
|
assert (
|
|
|
|
|
(reap_request is None)
|
|
|
|
|
==
|
|
|
|
|
(reaped is None)
|
|
|
|
|
)
|
2026-08-18 05:58:45 +00:00
|
|
|
if reaped is not None:
|
|
|
|
|
reaped.set()
|
|
|
|
|
|
|
|
|
|
async def _cancel_and_reap_child(
|
|
|
|
|
self,
|
|
|
|
|
portal: Portal,
|
|
|
|
|
) -> None:
|
|
|
|
|
'''
|
|
|
|
|
Cancel, join and unregister one nursery-owned child.
|
|
|
|
|
|
|
|
|
|
'''
|
2026-08-25 01:56:00 +00:00
|
|
|
aid: Aid = portal.channel.aid
|
|
|
|
|
uid: tuple[str, str] = aid.uid
|
2026-08-18 05:58:45 +00:00
|
|
|
child_entry = self._children.get(uid)
|
|
|
|
|
if child_entry is None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
subactor, proc, _ = child_entry
|
2026-08-25 01:56:00 +00:00
|
|
|
reap_request: trio.Event = self._child_reap_requests[aid]
|
|
|
|
|
reaped: trio.Event = self._child_reaped[aid]
|
2026-08-18 05:58:45 +00:00
|
|
|
|
|
|
|
|
with trio.CancelScope(shield=True):
|
|
|
|
|
try:
|
|
|
|
|
await _try_cancel_then_kill(
|
|
|
|
|
portal,
|
|
|
|
|
proc,
|
|
|
|
|
subactor,
|
|
|
|
|
self._at_least_one_child_in_debug,
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
reap_request.set()
|
|
|
|
|
await reaped.wait()
|
|
|
|
|
|
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,
|
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
|
2026-08-21 05:20:08 +00:00
|
|
|
if self._cancel_called:
|
|
|
|
|
raise RuntimeError(
|
|
|
|
|
'Cannot start an actor in a cancelling '
|
|
|
|
|
'`ActorNursery`'
|
|
|
|
|
)
|
|
|
|
|
|
2024-04-18 19:17:50 +00:00
|
|
|
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
|
2026-08-18 05:58:45 +00:00
|
|
|
self._at_least_one_child_in_debug |= debug_mode
|
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
|
2020-01-21 02:06:49 +00:00
|
|
|
# XXX: the type ignore is actually due to a `mypy` bug
|
2026-07-02 21:47:46 +00:00
|
|
|
return await self._da_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
|
|
|
# @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`..
|
2026-08-18 05:58:45 +00:00
|
|
|
children: tuple = tuple(self._children.values())
|
2024-03-01 20:44:01 +00:00
|
|
|
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,
|
2026-08-18 05:58:45 +00:00
|
|
|
) in children:
|
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
|
2026-08-18 05:58:45 +00:00
|
|
|
_, _, portal = self._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,
|
2026-08-18 05:58:45 +00:00
|
|
|
) in children:
|
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
|
2026-08-18 05:58:45 +00:00
|
|
|
self._request_reap_all()
|
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
|
|
|
|
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
|
|
|
|
2026-07-02 21:47:46 +00:00
|
|
|
# The single "daemon actor" nursery into which ALL subactors
|
Remove `run_in_actor()` + the ria reap cluster
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-07-06 16:52:29 +00:00
|
|
|
# are spawned; one-shot (`to_actor.run()`) subactors are
|
|
|
|
|
# result-waited and reaped in their caller's own task-scope
|
|
|
|
|
# (see the #477 `.run_in_actor()`/`._ria_nursery` removal);
|
|
|
|
|
# errors from this nursery bubble up to the caller.
|
2025-06-16 17:23:54 +00:00
|
|
|
async with (
|
|
|
|
|
collapse_eg(),
|
|
|
|
|
trio.open_nursery() as da_nursery,
|
|
|
|
|
):
|
2026-07-02 21:47:46 +00:00
|
|
|
an = ActorNursery(
|
|
|
|
|
actor,
|
|
|
|
|
da_nursery,
|
|
|
|
|
errors
|
|
|
|
|
)
|
2021-02-24 17:59:43 +00:00
|
|
|
try:
|
2026-07-02 22:33:17 +00:00
|
|
|
# spawning of actors happens in the caller's scope
|
|
|
|
|
# after we yield upwards
|
|
|
|
|
yield an
|
|
|
|
|
|
|
|
|
|
# When we didn't error in the caller's scope,
|
|
|
|
|
# signal all process-monitor-tasks to conduct
|
|
|
|
|
# the "hard join phase".
|
|
|
|
|
log.runtime(
|
|
|
|
|
'Waiting on subactors to complete:\n'
|
|
|
|
|
f'>}} {len(an._children)}\n'
|
|
|
|
|
)
|
|
|
|
|
an._request_reap_all()
|
|
|
|
|
|
|
|
|
|
# Single one-cancels-all handler for the (now single)
|
|
|
|
|
# daemon nursery. Pre-#477 a 2ndary `._ria_nursery`
|
|
|
|
|
# required a separate *outer* handler to catch errors
|
|
|
|
|
# bubbling from its task-reaping `__aexit__`; with that
|
|
|
|
|
# nursery gone this lone handler covers every scope
|
|
|
|
|
# error. NB: we deliberately do NOT re-raise here — the
|
|
|
|
|
# `finally` below raises the collected `errors` (as a
|
|
|
|
|
# single exc or `BaseExceptionGroup`), which already
|
|
|
|
|
# superseded the old outer handler's `raise` anyway
|
|
|
|
|
# since `errors` is populated (below) before any await.
|
|
|
|
|
except BaseException as _scope_err:
|
|
|
|
|
an._scope_error = _scope_err
|
|
|
|
|
errors[actor.aid.uid] = _scope_err
|
|
|
|
|
|
|
|
|
|
# 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-12-10 16:54:27 +00:00
|
|
|
|
2026-07-02 22:33:17 +00:00
|
|
|
# if the caller's scope errored then we activate our
|
|
|
|
|
# one-cancels-all supervisor strategy (don't
|
|
|
|
|
# worry more are coming).
|
|
|
|
|
an._request_reap_all()
|
|
|
|
|
|
|
|
|
|
# XXX NOTE XXX: hypothetically an error could
|
|
|
|
|
# be raised and then a cancel signal shows up
|
|
|
|
|
# slightly after in which case the `else:`
|
|
|
|
|
# block here might not complete? For now,
|
|
|
|
|
# shield both.
|
|
|
|
|
with trio.CancelScope(shield=True):
|
|
|
|
|
etype: type = type(_scope_err)
|
|
|
|
|
if etype in (
|
|
|
|
|
trio.Cancelled,
|
|
|
|
|
KeyboardInterrupt,
|
|
|
|
|
) or (
|
|
|
|
|
is_multi_cancelled(_scope_err)
|
|
|
|
|
):
|
|
|
|
|
log.cancel(
|
|
|
|
|
f'Actor-nursery cancelled by {etype}\n\n'
|
|
|
|
|
|
|
|
|
|
f'{current_actor().aid.uid}\n'
|
|
|
|
|
f' |_{an}\n\n'
|
|
|
|
|
|
|
|
|
|
# TODO: show tb str?
|
|
|
|
|
# f'{tb_str}'
|
|
|
|
|
)
|
|
|
|
|
elif etype in {
|
|
|
|
|
ContextCancelled,
|
|
|
|
|
}:
|
|
|
|
|
log.cancel(
|
|
|
|
|
'Actor-nursery caught remote cancellation\n'
|
|
|
|
|
'\n'
|
|
|
|
|
f'{_scope_err.tb_str}'
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
log.exception(
|
|
|
|
|
'Nursery errored with:\n'
|
|
|
|
|
|
|
|
|
|
# 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..
|
|
|
|
|
# '------ - ------'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# cancel all subactors
|
|
|
|
|
await an.cancel()
|
|
|
|
|
|
2021-02-24 17:59:43 +00:00
|
|
|
finally:
|
Remove `run_in_actor()` + the ria reap cluster
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-07-06 16:52:29 +00:00
|
|
|
# an error was stashed by the handler above (or by
|
|
|
|
|
# a spawn task via the shared `errors` dict) so
|
|
|
|
|
# cancel any remaining subactors, summarize and
|
|
|
|
|
# re-raise.
|
2021-02-24 17:59:43 +00:00
|
|
|
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
|
|
|
|
Remove `run_in_actor()` + the ria reap cluster
The final excision of #477: with zero in-repo callers left (all
tests/examples/docs migrated to `to_actor.run()` et al) the
entire legacy one-shot machinery drops out,
- `runtime/_supervise.py`: `ActorNursery.run_in_actor()`, the
`._cancel_after_result_on_exit` portal-set and the
`_reap_ria_portals()` teardown-reaper (both its happy-path
block-exit call AND the error-path snapshot + 0.5s-bounded
collection) are deleted — one-shot result-waiting now lives
entirely in the caller's task via `to_actor.run()`, whose
enclosing cancel-scope bounds the wait by construction (the
correct-scoping fix for the unbounded-reap hang class; the
`d1fb4a1a` guard test now passes structurally).
- `runtime/_portal.py`: `Portal._submit_for_result()`,
`._expect_result_ctx`, `._final_result_msg/_pld`,
`.wait_for_result()` + the deprecated `.result()` alias are
gone — a `Portal` no longer has any "main result" notion.
NB `Context.wait_for_result()` is a different (very alive)
API and is untouched.
- `spawn/_spawn.py`: `exhaust_portal()` +
`cancel_on_completion()` (the reaper tasks) deleted; backend
comment sweeps in `_trio.py`/`_mp.py`.
- `_exceptions.py`: the `NoResult` sentinel dies with its lone
reader.
- `tests/test_ringbuf.py`: drop a daemon-portal `.result()`
call that was already a warn + `NoResult` no-op (the ctx-acm
exit does the real result-wait); unshadow the 2nd `sctx` as
`rctx`.
- comment/docstring x-ref sweeps: `msg/types.py`,
`_context.py`, `to_actor/`, `tests/test_to_actor.py`.
Gate: `test_to_actor test_spawning test_cancellation
test_infected_asyncio test_local test_rpc` = 81 passed,
3 xfailed on `trio`; +`test_ringbuf` = 70 passed, 3 skipped,
3 xfailed on `mp_spawn`.
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-07-06 16:52:29 +00:00
|
|
|
When an actor is spawned a new trio task invokes one of the
|
|
|
|
|
process spawning backends to create and start a new subprocess.
|
|
|
|
|
These tasks are started in the supervisor's process nursery.
|
|
|
|
|
Spawning from a task is required because ``trio_run_in_process``
|
|
|
|
|
creates an internal nursery which the opening task **must** close;
|
|
|
|
|
this also makes each task's cancellation scope correspond to its
|
|
|
|
|
spawned subactor.
|
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)
|