forked from goodboy/tractor
commit
ae6d751d71
|
@ -0,0 +1,16 @@
|
||||||
|
This (finally) adds fully acknowledged remote cancellation messaging
|
||||||
|
support for both explicit ``Portal.cancel_actor()`` calls as well as
|
||||||
|
when there is a "runtime-wide" cancellations (eg. during KBI or general
|
||||||
|
actor nursery exception handling which causes a full actor
|
||||||
|
"crash"/termination).
|
||||||
|
|
||||||
|
You can think of this as the most ideal case in 2-generals where the
|
||||||
|
actor requesting the cancel of its child is able to always receive back
|
||||||
|
the ACK to that request. This leads to a more deterministic shutdown of
|
||||||
|
the child where the parent is able to wait for the child to fully
|
||||||
|
respond to the request. On a localhost setup, where the parent can
|
||||||
|
monitor the state of the child through process or other OS APIs instead
|
||||||
|
of solely through IPC messaging, the parent can know whether or not the
|
||||||
|
child decided to cancel with more certainty. In the case of separate
|
||||||
|
hosts, we still rely on a simple timeout approach until such a time
|
||||||
|
where we prefer to get "fancier".
|
|
@ -128,7 +128,11 @@ def test_multierror_fast_nursery(arb_addr, start_method, num_subactors, delay):
|
||||||
|
|
||||||
if len(exceptions) == 2:
|
if len(exceptions) == 2:
|
||||||
# sometimes oddly now there's an embedded BrokenResourceError ?
|
# sometimes oddly now there's an embedded BrokenResourceError ?
|
||||||
exceptions = exceptions[1].exceptions
|
for exc in exceptions:
|
||||||
|
excs = getattr(exc, 'exceptions', None)
|
||||||
|
if excs:
|
||||||
|
exceptions = excs
|
||||||
|
break
|
||||||
|
|
||||||
assert len(exceptions) == num_subactors
|
assert len(exceptions) == num_subactors
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Actor primitives and helpers
|
Actor primitives and helpers
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
@ -10,7 +11,7 @@ import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import uuid
|
import uuid
|
||||||
import typing
|
import typing
|
||||||
from typing import Dict, List, Tuple, Any, Optional, Union
|
from typing import List, Tuple, Any, Optional, Union
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
@ -48,7 +49,7 @@ async def _invoke(
|
||||||
cid: str,
|
cid: str,
|
||||||
chan: Channel,
|
chan: Channel,
|
||||||
func: typing.Callable,
|
func: typing.Callable,
|
||||||
kwargs: Dict[str, Any],
|
kwargs: dict[str, Any],
|
||||||
is_rpc: bool = True,
|
is_rpc: bool = True,
|
||||||
task_status: TaskStatus[
|
task_status: TaskStatus[
|
||||||
Union[trio.CancelScope, BaseException]
|
Union[trio.CancelScope, BaseException]
|
||||||
|
@ -57,6 +58,8 @@ async def _invoke(
|
||||||
'''
|
'''
|
||||||
Invoke local func and deliver result(s) over provided channel.
|
Invoke local func and deliver result(s) over provided channel.
|
||||||
|
|
||||||
|
This is the core "RPC task" starting machinery.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
treat_as_gen = False
|
treat_as_gen = False
|
||||||
|
@ -263,14 +266,51 @@ def _get_mod_abspath(module):
|
||||||
_lifetime_stack: ExitStack = ExitStack()
|
_lifetime_stack: ExitStack = ExitStack()
|
||||||
|
|
||||||
|
|
||||||
class Actor:
|
async def try_ship_error_to_parent(
|
||||||
"""The fundamental concurrency primitive.
|
channel: Channel,
|
||||||
|
err: Union[Exception, trio.MultiError],
|
||||||
|
|
||||||
An *actor* is the combination of a regular Python process
|
) -> None:
|
||||||
executing a ``trio`` task tree, communicating
|
with trio.CancelScope(shield=True):
|
||||||
with other actors through "portals" which provide a native async API
|
try:
|
||||||
around various IPC transport "channels".
|
# internal error so ship to parent without cid
|
||||||
"""
|
await channel.send(pack_error(err))
|
||||||
|
except (
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
trio.BrokenResourceError,
|
||||||
|
):
|
||||||
|
log.error(
|
||||||
|
f"Failed to ship error to parent "
|
||||||
|
f"{channel.uid}, channel was closed"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Actor:
|
||||||
|
'''
|
||||||
|
The fundamental "runtime" concurrency primitive.
|
||||||
|
|
||||||
|
An *actor* is the combination of a regular Python process executing
|
||||||
|
a ``trio`` task tree, communicating with other actors through
|
||||||
|
"memory boundary portals" - which provide a native async API around
|
||||||
|
IPC transport "channels" which themselves encapsulate various
|
||||||
|
(swappable) network protocols.
|
||||||
|
|
||||||
|
|
||||||
|
Each "actor" is ``trio.run()`` scheduled "runtime" composed of many
|
||||||
|
concurrent tasks in a single thread. The "runtime" tasks conduct
|
||||||
|
a slew of low(er) level functions to make it possible for message
|
||||||
|
passing between actors as well as the ability to create new actors
|
||||||
|
(aka new "runtimes" in new processes which are supervised via
|
||||||
|
a nursery construct). Each task which sends messages to a task in
|
||||||
|
a "peer" (not necessarily a parent-child, depth hierarchy)) is able
|
||||||
|
to do so via an "address", which maps IPC connections across memory
|
||||||
|
boundaries, and task request id which allows for per-actor
|
||||||
|
tasks to send and receive messages to specific peer-actor tasks with
|
||||||
|
which there is an ongoing RPC/IPC dialog.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# ugh, we need to get rid of this and replace with a "registry" sys
|
||||||
|
# https://github.com/goodboy/tractor/issues/216
|
||||||
is_arbiter: bool = False
|
is_arbiter: bool = False
|
||||||
|
|
||||||
# nursery placeholders filled in by `_async_main()` after fork
|
# nursery placeholders filled in by `_async_main()` after fork
|
||||||
|
@ -279,7 +319,7 @@ class Actor:
|
||||||
_server_n: Optional[trio.Nursery] = None
|
_server_n: Optional[trio.Nursery] = None
|
||||||
|
|
||||||
# Information about `__main__` from parent
|
# Information about `__main__` from parent
|
||||||
_parent_main_data: Dict[str, str]
|
_parent_main_data: dict[str, str]
|
||||||
_parent_chan_cs: Optional[trio.CancelScope] = None
|
_parent_chan_cs: Optional[trio.CancelScope] = None
|
||||||
|
|
||||||
# syncs for setup/teardown sequences
|
# syncs for setup/teardown sequences
|
||||||
|
@ -317,7 +357,7 @@ class Actor:
|
||||||
mods[name] = _get_mod_abspath(mod)
|
mods[name] = _get_mod_abspath(mod)
|
||||||
|
|
||||||
self.enable_modules = mods
|
self.enable_modules = mods
|
||||||
self._mods: Dict[str, ModuleType] = {}
|
self._mods: dict[str, ModuleType] = {}
|
||||||
|
|
||||||
# TODO: consider making this a dynamically defined
|
# TODO: consider making this a dynamically defined
|
||||||
# @dataclass once we get py3.7
|
# @dataclass once we get py3.7
|
||||||
|
@ -340,12 +380,12 @@ class Actor:
|
||||||
self._ongoing_rpc_tasks = trio.Event()
|
self._ongoing_rpc_tasks = trio.Event()
|
||||||
self._ongoing_rpc_tasks.set()
|
self._ongoing_rpc_tasks.set()
|
||||||
# (chan, cid) -> (cancel_scope, func)
|
# (chan, cid) -> (cancel_scope, func)
|
||||||
self._rpc_tasks: Dict[
|
self._rpc_tasks: dict[
|
||||||
Tuple[Channel, str],
|
Tuple[Channel, str],
|
||||||
Tuple[trio.CancelScope, typing.Callable, trio.Event]
|
Tuple[trio.CancelScope, typing.Callable, trio.Event]
|
||||||
] = {}
|
] = {}
|
||||||
# map {uids -> {callids -> waiter queues}}
|
# map {uids -> {callids -> waiter queues}}
|
||||||
self._cids2qs: Dict[
|
self._cids2qs: dict[
|
||||||
Tuple[Tuple[str, str], str],
|
Tuple[Tuple[str, str], str],
|
||||||
Tuple[
|
Tuple[
|
||||||
trio.abc.SendChannel[Any],
|
trio.abc.SendChannel[Any],
|
||||||
|
@ -356,7 +396,7 @@ class Actor:
|
||||||
self._parent_chan: Optional[Channel] = None
|
self._parent_chan: Optional[Channel] = None
|
||||||
self._forkserver_info: Optional[
|
self._forkserver_info: Optional[
|
||||||
Tuple[Any, Any, Any, Any, Any]] = None
|
Tuple[Any, Any, Any, Any, Any]] = None
|
||||||
self._actoruid2nursery: Dict[str, 'ActorNursery'] = {} # type: ignore # noqa
|
self._actoruid2nursery: dict[Optional[tuple[str, str]], 'ActorNursery'] = {} # type: ignore # noqa
|
||||||
|
|
||||||
async def wait_for_peer(
|
async def wait_for_peer(
|
||||||
self, uid: Tuple[str, str]
|
self, uid: Tuple[str, str]
|
||||||
|
@ -441,8 +481,8 @@ class Actor:
|
||||||
# we need this for ``msgspec`` for some reason?
|
# we need this for ``msgspec`` for some reason?
|
||||||
# for now, it's been put in the stream backend.
|
# for now, it's been put in the stream backend.
|
||||||
# trio.BrokenResourceError,
|
# trio.BrokenResourceError,
|
||||||
|
|
||||||
# trio.ClosedResourceError,
|
# trio.ClosedResourceError,
|
||||||
|
|
||||||
TransportClosed,
|
TransportClosed,
|
||||||
):
|
):
|
||||||
# XXX: This may propagate up from ``Channel._aiter_recv()``
|
# XXX: This may propagate up from ``Channel._aiter_recv()``
|
||||||
|
@ -482,7 +522,50 @@ class Actor:
|
||||||
# process received reponses.
|
# process received reponses.
|
||||||
try:
|
try:
|
||||||
await self._process_messages(chan)
|
await self._process_messages(chan)
|
||||||
|
|
||||||
|
except trio.Cancelled:
|
||||||
|
log.cancel(f"Msg loop was cancelled for {chan}")
|
||||||
|
raise
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
|
# This is set in ``Portal.cancel_actor()``. So if
|
||||||
|
# the peer was cancelled we try to wait for them
|
||||||
|
# to tear down their side of the connection before
|
||||||
|
# moving on with closing our own side.
|
||||||
|
local_nursery = self._actoruid2nursery.get(chan.uid)
|
||||||
|
if (
|
||||||
|
local_nursery
|
||||||
|
):
|
||||||
|
log.cancel(f"Waiting on cancel request to peer {chan.uid}")
|
||||||
|
# XXX: this is a soft wait on the channel (and its
|
||||||
|
# underlying transport protocol) to close from the remote
|
||||||
|
# peer side since we presume that any channel which
|
||||||
|
# is mapped to a sub-actor (i.e. it's managed by
|
||||||
|
# one of our local nurseries)
|
||||||
|
# message is sent to the peer likely by this actor which is
|
||||||
|
# now in a cancelled condition) when the local runtime here
|
||||||
|
# is now cancelled while (presumably) in the middle of msg
|
||||||
|
# loop processing.
|
||||||
|
with trio.move_on_after(0.1) as cs:
|
||||||
|
cs.shield = True
|
||||||
|
# Attempt to wait for the far end to close the channel
|
||||||
|
# and bail after timeout (2-generals on closure).
|
||||||
|
assert chan.msgstream
|
||||||
|
async for msg in chan.msgstream.drain():
|
||||||
|
# try to deliver any lingering msgs
|
||||||
|
# before we destroy the channel.
|
||||||
|
# This accomplishes deterministic
|
||||||
|
# ``Portal.cancel_actor()`` cancellation by
|
||||||
|
# making sure any RPC response to that call is
|
||||||
|
# delivered the local calling task.
|
||||||
|
# TODO: factor this into a helper?
|
||||||
|
log.runtime(f'drained {msg} for {chan.uid}')
|
||||||
|
cid = msg.get('cid')
|
||||||
|
if cid:
|
||||||
|
# deliver response to local caller/waiter
|
||||||
|
await self._push_result(chan, cid, msg)
|
||||||
|
|
||||||
|
await local_nursery.exited.wait()
|
||||||
|
|
||||||
# channel cleanup sequence
|
# channel cleanup sequence
|
||||||
|
|
||||||
|
@ -534,7 +617,7 @@ class Actor:
|
||||||
self,
|
self,
|
||||||
chan: Channel,
|
chan: Channel,
|
||||||
cid: str,
|
cid: str,
|
||||||
msg: Dict[str, Any],
|
msg: dict[str, Any],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Push an RPC result to the local consumer's queue.
|
"""Push an RPC result to the local consumer's queue.
|
||||||
"""
|
"""
|
||||||
|
@ -593,10 +676,12 @@ class Actor:
|
||||||
func: str,
|
func: str,
|
||||||
kwargs: dict
|
kwargs: dict
|
||||||
) -> Tuple[str, trio.abc.ReceiveChannel]:
|
) -> Tuple[str, trio.abc.ReceiveChannel]:
|
||||||
"""Send a ``'cmd'`` message to a remote actor and return a
|
'''
|
||||||
|
Send a ``'cmd'`` message to a remote actor and return a
|
||||||
caller id and a ``trio.Queue`` that can be used to wait for
|
caller id and a ``trio.Queue`` that can be used to wait for
|
||||||
responses delivered by the local message processing loop.
|
responses delivered by the local message processing loop.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
cid = str(uuid.uuid4())
|
cid = str(uuid.uuid4())
|
||||||
assert chan.uid
|
assert chan.uid
|
||||||
send_chan, recv_chan = self.get_memchans(chan.uid, cid)
|
send_chan, recv_chan = self.get_memchans(chan.uid, cid)
|
||||||
|
@ -609,11 +694,14 @@ class Actor:
|
||||||
chan: Channel,
|
chan: Channel,
|
||||||
shield: bool = False,
|
shield: bool = False,
|
||||||
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED,
|
task_status: TaskStatus[trio.CancelScope] = trio.TASK_STATUS_IGNORED,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Process messages for the channel async-RPC style.
|
'''
|
||||||
|
Process messages for the channel async-RPC style.
|
||||||
|
|
||||||
Receive multiplexed RPC requests and deliver responses over ``chan``.
|
Receive multiplexed RPC requests and deliver responses over ``chan``.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
# TODO: once https://github.com/python-trio/trio/issues/467 gets
|
# TODO: once https://github.com/python-trio/trio/issues/467 gets
|
||||||
# worked out we'll likely want to use that!
|
# worked out we'll likely want to use that!
|
||||||
msg = None
|
msg = None
|
||||||
|
@ -692,8 +780,9 @@ class Actor:
|
||||||
# msg loop and break out into
|
# msg loop and break out into
|
||||||
# ``_async_main()``
|
# ``_async_main()``
|
||||||
log.cancel(
|
log.cancel(
|
||||||
f"Actor {self.uid} was remotely cancelled;"
|
f"Actor {self.uid} was remotely cancelled "
|
||||||
" waiting on cancellation completion..")
|
f"by {chan.uid}"
|
||||||
|
)
|
||||||
await _invoke(
|
await _invoke(
|
||||||
self, cid, chan, func, kwargs, is_rpc=False
|
self, cid, chan, func, kwargs, is_rpc=False
|
||||||
)
|
)
|
||||||
|
@ -789,17 +878,12 @@ class Actor:
|
||||||
# machinery not from an rpc task) to parent
|
# machinery not from an rpc task) to parent
|
||||||
log.exception("Actor errored:")
|
log.exception("Actor errored:")
|
||||||
if self._parent_chan:
|
if self._parent_chan:
|
||||||
await self._parent_chan.send(pack_error(err))
|
await try_ship_error_to_parent(self._parent_chan, err)
|
||||||
|
|
||||||
# if this is the `MainProcess` we expect the error broadcasting
|
# if this is the `MainProcess` we expect the error broadcasting
|
||||||
# above to trigger an error at consuming portal "checkpoints"
|
# above to trigger an error at consuming portal "checkpoints"
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except trio.Cancelled:
|
|
||||||
# debugging only
|
|
||||||
log.runtime(f"Msg loop was cancelled for {chan}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
# msg debugging for when he machinery is brokey
|
# msg debugging for when he machinery is brokey
|
||||||
log.runtime(
|
log.runtime(
|
||||||
|
@ -891,6 +975,7 @@ class Actor:
|
||||||
# establish primary connection with immediate parent
|
# establish primary connection with immediate parent
|
||||||
self._parent_chan = None
|
self._parent_chan = None
|
||||||
if parent_addr is not None:
|
if parent_addr is not None:
|
||||||
|
|
||||||
self._parent_chan, accept_addr_rent = await self._from_parent(
|
self._parent_chan, accept_addr_rent = await self._from_parent(
|
||||||
parent_addr)
|
parent_addr)
|
||||||
|
|
||||||
|
@ -994,14 +1079,7 @@ class Actor:
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._parent_chan:
|
if self._parent_chan:
|
||||||
with trio.CancelScope(shield=True):
|
await try_ship_error_to_parent(self._parent_chan, err)
|
||||||
try:
|
|
||||||
# internal error so ship to parent without cid
|
|
||||||
await self._parent_chan.send(pack_error(err))
|
|
||||||
except trio.ClosedResourceError:
|
|
||||||
log.error(
|
|
||||||
f"Failed to ship error to parent "
|
|
||||||
f"{self._parent_chan.uid}, channel was closed")
|
|
||||||
|
|
||||||
# always!
|
# always!
|
||||||
log.exception("Actor errored:")
|
log.exception("Actor errored:")
|
||||||
|
@ -1283,7 +1361,7 @@ class Arbiter(Actor):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
self._registry: Dict[
|
self._registry: dict[
|
||||||
Tuple[str, str],
|
Tuple[str, str],
|
||||||
Tuple[str, int],
|
Tuple[str, int],
|
||||||
] = {}
|
] = {}
|
||||||
|
@ -1300,7 +1378,7 @@ class Arbiter(Actor):
|
||||||
|
|
||||||
async def get_registry(
|
async def get_registry(
|
||||||
self
|
self
|
||||||
) -> Dict[Tuple[str, str], Tuple[str, int]]:
|
) -> dict[Tuple[str, str], Tuple[str, int]]:
|
||||||
'''Return current name registry.
|
'''Return current name registry.
|
||||||
|
|
||||||
This method is async to allow for cross-actor invocation.
|
This method is async to allow for cross-actor invocation.
|
||||||
|
|
|
@ -6,9 +6,10 @@ from __future__ import annotations
|
||||||
import platform
|
import platform
|
||||||
import struct
|
import struct
|
||||||
import typing
|
import typing
|
||||||
|
from collections.abc import AsyncGenerator, AsyncIterator
|
||||||
from typing import (
|
from typing import (
|
||||||
Any, Tuple, Optional,
|
Any, Tuple, Optional,
|
||||||
Type, Protocol, TypeVar
|
Type, Protocol, TypeVar,
|
||||||
)
|
)
|
||||||
|
|
||||||
from tricycle import BufferedReceiveStream
|
from tricycle import BufferedReceiveStream
|
||||||
|
@ -46,6 +47,7 @@ MsgType = TypeVar("MsgType")
|
||||||
class MsgTransport(Protocol[MsgType]):
|
class MsgTransport(Protocol[MsgType]):
|
||||||
|
|
||||||
stream: trio.SocketStream
|
stream: trio.SocketStream
|
||||||
|
drained: list[MsgType]
|
||||||
|
|
||||||
def __init__(self, stream: trio.SocketStream) -> None:
|
def __init__(self, stream: trio.SocketStream) -> None:
|
||||||
...
|
...
|
||||||
|
@ -63,6 +65,11 @@ class MsgTransport(Protocol[MsgType]):
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
# defining this sync otherwise it causes a mypy error because it
|
||||||
|
# can't figure out it's a generator i guess?..?
|
||||||
|
def drain(self) -> AsyncIterator[dict]:
|
||||||
|
...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def laddr(self) -> Tuple[str, int]:
|
def laddr(self) -> Tuple[str, int]:
|
||||||
...
|
...
|
||||||
|
@ -93,7 +100,10 @@ class MsgpackTCPStream:
|
||||||
self._agen = self._iter_packets()
|
self._agen = self._iter_packets()
|
||||||
self._send_lock = trio.StrictFIFOLock()
|
self._send_lock = trio.StrictFIFOLock()
|
||||||
|
|
||||||
async def _iter_packets(self) -> typing.AsyncGenerator[dict, None]:
|
# public i guess?
|
||||||
|
self.drained: list[dict] = []
|
||||||
|
|
||||||
|
async def _iter_packets(self) -> AsyncGenerator[dict, None]:
|
||||||
"""Yield packets from the underlying stream.
|
"""Yield packets from the underlying stream.
|
||||||
"""
|
"""
|
||||||
unpacker = msgpack.Unpacker(
|
unpacker = msgpack.Unpacker(
|
||||||
|
@ -132,7 +142,7 @@ class MsgpackTCPStream:
|
||||||
|
|
||||||
if data == b'':
|
if data == b'':
|
||||||
raise TransportClosed(
|
raise TransportClosed(
|
||||||
f'transport {self} was already closed prior ro read'
|
f'transport {self} was already closed prior to read'
|
||||||
)
|
)
|
||||||
|
|
||||||
unpacker.feed(data)
|
unpacker.feed(data)
|
||||||
|
@ -156,6 +166,20 @@ class MsgpackTCPStream:
|
||||||
async def recv(self) -> Any:
|
async def recv(self) -> Any:
|
||||||
return await self._agen.asend(None)
|
return await self._agen.asend(None)
|
||||||
|
|
||||||
|
async def drain(self) -> AsyncIterator[dict]:
|
||||||
|
'''
|
||||||
|
Drain the stream's remaining messages sent from
|
||||||
|
the far end until the connection is closed by
|
||||||
|
the peer.
|
||||||
|
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
async for msg in self._iter_packets():
|
||||||
|
self.drained.append(msg)
|
||||||
|
except TransportClosed:
|
||||||
|
for msg in self.drained:
|
||||||
|
yield msg
|
||||||
|
|
||||||
def __aiter__(self):
|
def __aiter__(self):
|
||||||
return self._agen
|
return self._agen
|
||||||
|
|
||||||
|
@ -164,7 +188,8 @@ class MsgpackTCPStream:
|
||||||
|
|
||||||
|
|
||||||
class MsgspecTCPStream(MsgpackTCPStream):
|
class MsgspecTCPStream(MsgpackTCPStream):
|
||||||
'''A ``trio.SocketStream`` delivering ``msgpack`` formatted data
|
'''
|
||||||
|
A ``trio.SocketStream`` delivering ``msgpack`` formatted data
|
||||||
using ``msgspec``.
|
using ``msgspec``.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -184,7 +209,7 @@ class MsgspecTCPStream(MsgpackTCPStream):
|
||||||
self.encode = msgspec.Encoder().encode
|
self.encode = msgspec.Encoder().encode
|
||||||
self.decode = msgspec.Decoder().decode # dict[str, Any])
|
self.decode = msgspec.Decoder().decode # dict[str, Any])
|
||||||
|
|
||||||
async def _iter_packets(self) -> typing.AsyncGenerator[dict, None]:
|
async def _iter_packets(self) -> AsyncGenerator[dict, None]:
|
||||||
'''Yield packets from the underlying stream.
|
'''Yield packets from the underlying stream.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
@ -259,9 +284,12 @@ def get_msg_transport(
|
||||||
|
|
||||||
|
|
||||||
class Channel:
|
class Channel:
|
||||||
'''An inter-process channel for communication between (remote) actors.
|
'''
|
||||||
|
An inter-process channel for communication between (remote) actors.
|
||||||
|
|
||||||
Currently the only supported transport is a ``trio.SocketStream``.
|
Wraps a ``MsgStream``: transport + encoding IPC connection.
|
||||||
|
Currently we only support ``trio.SocketStream`` for transport
|
||||||
|
(aka TCP).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -299,10 +327,12 @@ class Channel:
|
||||||
# set after handshake - always uid of far end
|
# set after handshake - always uid of far end
|
||||||
self.uid: Optional[Tuple[str, str]] = None
|
self.uid: Optional[Tuple[str, str]] = None
|
||||||
|
|
||||||
# set if far end actor errors internally
|
|
||||||
self._exc: Optional[Exception] = None
|
|
||||||
self._agen = self._aiter_recv()
|
self._agen = self._aiter_recv()
|
||||||
|
self._exc: Optional[Exception] = None # set if far end actor errors
|
||||||
self._closed: bool = False
|
self._closed: bool = False
|
||||||
|
# flag set on ``Portal.cancel_actor()`` indicating
|
||||||
|
# remote (peer) cancellation of the far end actor runtime.
|
||||||
|
self._cancel_called: bool = False # set on ``Portal.cancel_actor()``
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_stream(
|
def from_stream(
|
||||||
|
@ -441,9 +471,11 @@ class Channel:
|
||||||
|
|
||||||
async def _aiter_recv(
|
async def _aiter_recv(
|
||||||
self
|
self
|
||||||
) -> typing.AsyncGenerator[Any, None]:
|
) -> AsyncGenerator[Any, None]:
|
||||||
"""Async iterate items from underlying stream.
|
'''
|
||||||
"""
|
Async iterate items from underlying stream.
|
||||||
|
|
||||||
|
'''
|
||||||
assert self.msgstream
|
assert self.msgstream
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
@ -473,9 +505,11 @@ class Channel:
|
||||||
async def _connect_chan(
|
async def _connect_chan(
|
||||||
host: str, port: int
|
host: str, port: int
|
||||||
) -> typing.AsyncGenerator[Channel, None]:
|
) -> typing.AsyncGenerator[Channel, None]:
|
||||||
"""Create and connect a channel with disconnect on context manager
|
'''
|
||||||
|
Create and connect a channel with disconnect on context manager
|
||||||
teardown.
|
teardown.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
chan = Channel((host, port))
|
chan = Channel((host, port))
|
||||||
await chan.connect()
|
await chan.connect()
|
||||||
yield chan
|
yield chan
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Portal api
|
Memory boundary "Portals": an API for structured
|
||||||
|
concurrency linked tasks running in disparate memory domains.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import importlib
|
import importlib
|
||||||
|
@ -21,7 +22,6 @@ from .log import get_logger
|
||||||
from ._exceptions import (
|
from ._exceptions import (
|
||||||
unpack_error,
|
unpack_error,
|
||||||
NoResult,
|
NoResult,
|
||||||
# RemoteActorError,
|
|
||||||
ContextCancelled,
|
ContextCancelled,
|
||||||
)
|
)
|
||||||
from ._streaming import Context, ReceiveMsgStream
|
from ._streaming import Context, ReceiveMsgStream
|
||||||
|
@ -35,10 +35,12 @@ async def maybe_open_nursery(
|
||||||
nursery: trio.Nursery = None,
|
nursery: trio.Nursery = None,
|
||||||
shield: bool = False,
|
shield: bool = False,
|
||||||
) -> AsyncGenerator[trio.Nursery, Any]:
|
) -> AsyncGenerator[trio.Nursery, Any]:
|
||||||
"""Create a new nursery if None provided.
|
'''
|
||||||
|
Create a new nursery if None provided.
|
||||||
|
|
||||||
Blocks on exit as expected if no input nursery is provided.
|
Blocks on exit as expected if no input nursery is provided.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
if nursery is not None:
|
if nursery is not None:
|
||||||
yield nursery
|
yield nursery
|
||||||
else:
|
else:
|
||||||
|
@ -87,14 +89,18 @@ class Portal:
|
||||||
like having a "portal" between the seperate actor memory spaces.
|
like having a "portal" between the seperate actor memory spaces.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
# the timeout for a remote cancel request sent to
|
||||||
|
# a(n) (peer) actor.
|
||||||
|
cancel_timeout = 0.5
|
||||||
|
|
||||||
def __init__(self, channel: Channel) -> None:
|
def __init__(self, channel: Channel) -> None:
|
||||||
self.channel = channel
|
self.channel = channel
|
||||||
# when this is set to a tuple returned from ``_submit()`` then
|
|
||||||
# it is expected that ``result()`` will be awaited at some point
|
|
||||||
# during the portal's lifetime
|
# during the portal's lifetime
|
||||||
self._result_msg: Optional[dict] = None
|
self._result_msg: Optional[dict] = None
|
||||||
|
|
||||||
# set when _submit_for_result is called
|
# When this is set to a tuple returned from ``_submit()`` then
|
||||||
|
# it is expected that ``result()`` will be awaited at some
|
||||||
|
# point. Set when _submit_for_result is called
|
||||||
self._expect_result: Optional[
|
self._expect_result: Optional[
|
||||||
Tuple[str, Any, str, Dict[str, Any]]
|
Tuple[str, Any, str, Dict[str, Any]]
|
||||||
] = None
|
] = None
|
||||||
|
@ -199,36 +205,46 @@ class Portal:
|
||||||
# we'll need to .aclose all those channels here
|
# we'll need to .aclose all those channels here
|
||||||
await self._cancel_streams()
|
await self._cancel_streams()
|
||||||
|
|
||||||
async def cancel_actor(self):
|
async def cancel_actor(
|
||||||
"""Cancel the actor on the other end of this portal.
|
self,
|
||||||
"""
|
timeout: float = None,
|
||||||
if not self.channel.connected():
|
|
||||||
log.cancel("This portal is already closed can't cancel")
|
|
||||||
return False
|
|
||||||
|
|
||||||
await self._cancel_streams()
|
) -> bool:
|
||||||
|
'''
|
||||||
|
Cancel the actor on the other end of this portal.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if not self.channel.connected():
|
||||||
|
log.cancel("This channel is already closed can't cancel")
|
||||||
|
return False
|
||||||
|
|
||||||
log.cancel(
|
log.cancel(
|
||||||
f"Sending actor cancel request to {self.channel.uid} on "
|
f"Sending actor cancel request to {self.channel.uid} on "
|
||||||
f"{self.channel}")
|
f"{self.channel}")
|
||||||
|
|
||||||
|
self.channel._cancel_called = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# send cancel cmd - might not get response
|
# send cancel cmd - might not get response
|
||||||
# XXX: sure would be nice to make this work with a proper shield
|
# XXX: sure would be nice to make this work with a proper shield
|
||||||
with trio.move_on_after(0.5) as cancel_scope:
|
with trio.move_on_after(timeout or self.cancel_timeout) as cs:
|
||||||
cancel_scope.shield = True
|
cs.shield = True
|
||||||
|
|
||||||
await self.run_from_ns('self', 'cancel')
|
await self.run_from_ns('self', 'cancel')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if cancel_scope.cancelled_caught:
|
if cs.cancelled_caught:
|
||||||
log.cancel(f"May have failed to cancel {self.channel.uid}")
|
log.cancel(f"May have failed to cancel {self.channel.uid}")
|
||||||
|
|
||||||
# if we get here some weird cancellation case happened
|
# if we get here some weird cancellation case happened
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except trio.ClosedResourceError:
|
except (
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
trio.BrokenResourceError,
|
||||||
|
):
|
||||||
log.cancel(
|
log.cancel(
|
||||||
f"{self.channel} for {self.channel.uid} was already closed?")
|
f"{self.channel} for {self.channel.uid} was already closed or broken?")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def run_from_ns(
|
async def run_from_ns(
|
||||||
|
@ -237,7 +253,9 @@ class Portal:
|
||||||
function_name: str,
|
function_name: str,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Run a function from a (remote) namespace in a new task on the far-end actor.
|
'''
|
||||||
|
Run a function from a (remote) namespace in a new task on the
|
||||||
|
far-end actor.
|
||||||
|
|
||||||
This is a more explitcit way to run tasks in a remote-process
|
This is a more explitcit way to run tasks in a remote-process
|
||||||
actor using explicit object-path syntax. Hint: this is how
|
actor using explicit object-path syntax. Hint: this is how
|
||||||
|
@ -246,9 +264,11 @@ class Portal:
|
||||||
Note::
|
Note::
|
||||||
|
|
||||||
A special namespace `self` can be used to invoke `Actor`
|
A special namespace `self` can be used to invoke `Actor`
|
||||||
instance methods in the remote runtime. Currently this should only
|
instance methods in the remote runtime. Currently this
|
||||||
be used for `tractor` internals.
|
should only be used solely for ``tractor`` runtime
|
||||||
"""
|
internals.
|
||||||
|
|
||||||
|
'''
|
||||||
msg = await self._return_once(
|
msg = await self._return_once(
|
||||||
*(await self._submit(namespace_path, function_name, kwargs))
|
*(await self._submit(namespace_path, function_name, kwargs))
|
||||||
)
|
)
|
||||||
|
@ -447,7 +467,8 @@ class Portal:
|
||||||
except (
|
except (
|
||||||
BaseException,
|
BaseException,
|
||||||
|
|
||||||
# more specifically, we need to handle:
|
# more specifically, we need to handle these but not
|
||||||
|
# sure it's worth being pedantic:
|
||||||
# Exception,
|
# Exception,
|
||||||
# trio.Cancelled,
|
# trio.Cancelled,
|
||||||
# trio.MultiError,
|
# trio.MultiError,
|
||||||
|
@ -495,19 +516,22 @@ class Portal:
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalPortal:
|
class LocalPortal:
|
||||||
"""A 'portal' to a local ``Actor``.
|
'''
|
||||||
|
A 'portal' to a local ``Actor``.
|
||||||
|
|
||||||
A compatibility shim for normal portals but for invoking functions
|
A compatibility shim for normal portals but for invoking functions
|
||||||
using an in process actor instance.
|
using an in process actor instance.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
actor: 'Actor' # type: ignore # noqa
|
actor: 'Actor' # type: ignore # noqa
|
||||||
channel: Channel
|
channel: Channel
|
||||||
|
|
||||||
async def run_from_ns(self, ns: str, func_name: str, **kwargs) -> Any:
|
async def run_from_ns(self, ns: str, func_name: str, **kwargs) -> Any:
|
||||||
"""Run a requested local function from a namespace path and
|
'''
|
||||||
|
Run a requested local function from a namespace path and
|
||||||
return it's result.
|
return it's result.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
obj = self.actor if ns == 'self' else importlib.import_module(ns)
|
obj = self.actor if ns == 'self' else importlib.import_module(ns)
|
||||||
func = getattr(obj, func_name)
|
func = getattr(obj, func_name)
|
||||||
return await func(**kwargs)
|
return await func(**kwargs)
|
||||||
|
@ -522,10 +546,13 @@ async def open_portal(
|
||||||
shield: bool = False,
|
shield: bool = False,
|
||||||
|
|
||||||
) -> AsyncGenerator[Portal, None]:
|
) -> AsyncGenerator[Portal, None]:
|
||||||
"""Open a ``Portal`` through the provided ``channel``.
|
'''
|
||||||
|
Open a ``Portal`` through the provided ``channel``.
|
||||||
|
|
||||||
Spawns a background task to handle message processing.
|
Spawns a background task to handle message processing (normally
|
||||||
"""
|
done by the actor-runtime implicitly).
|
||||||
|
|
||||||
|
'''
|
||||||
actor = current_actor()
|
actor = current_actor()
|
||||||
assert actor
|
assert actor
|
||||||
was_connected = False
|
was_connected = False
|
||||||
|
@ -553,7 +580,6 @@ async def open_portal(
|
||||||
portal = Portal(channel)
|
portal = Portal(channel)
|
||||||
try:
|
try:
|
||||||
yield portal
|
yield portal
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
await portal.aclose()
|
await portal.aclose()
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@ Machinery for actor process spawning using multiple backends.
|
||||||
import sys
|
import sys
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import platform
|
import platform
|
||||||
from typing import Any, Dict, Optional
|
from typing import (
|
||||||
|
Any, Dict, Optional, Union, Callable,
|
||||||
|
TypeVar,
|
||||||
|
)
|
||||||
|
from collections.abc import Awaitable, Coroutine
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
from trio_typing import TaskStatus
|
from trio_typing import TaskStatus
|
||||||
|
@ -41,6 +45,7 @@ from ._exceptions import ActorFailure
|
||||||
|
|
||||||
|
|
||||||
log = get_logger('tractor')
|
log = get_logger('tractor')
|
||||||
|
ProcessType = TypeVar('ProcessType', mp.Process, trio.Process)
|
||||||
|
|
||||||
# placeholder for an mp start context if so using that backend
|
# placeholder for an mp start context if so using that backend
|
||||||
_ctx: Optional[mp.context.BaseContext] = None
|
_ctx: Optional[mp.context.BaseContext] = None
|
||||||
|
@ -97,14 +102,17 @@ def try_set_start_method(name: str) -> Optional[mp.context.BaseContext]:
|
||||||
|
|
||||||
|
|
||||||
async def exhaust_portal(
|
async def exhaust_portal(
|
||||||
|
|
||||||
portal: Portal,
|
portal: Portal,
|
||||||
actor: Actor
|
actor: Actor
|
||||||
|
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Pull final result from portal (assuming it has one).
|
'''
|
||||||
|
Pull final result from portal (assuming it has one).
|
||||||
|
|
||||||
If the main task is an async generator do our best to consume
|
If the main task is an async generator do our best to consume
|
||||||
what's left of it.
|
what's left of it.
|
||||||
"""
|
'''
|
||||||
try:
|
try:
|
||||||
log.debug(f"Waiting on final result from {actor.uid}")
|
log.debug(f"Waiting on final result from {actor.uid}")
|
||||||
|
|
||||||
|
@ -126,18 +134,19 @@ async def exhaust_portal(
|
||||||
|
|
||||||
|
|
||||||
async def cancel_on_completion(
|
async def cancel_on_completion(
|
||||||
|
|
||||||
portal: Portal,
|
portal: Portal,
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
errors: Dict[Tuple[str, str], Exception],
|
errors: Dict[Tuple[str, str], Exception],
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
'''
|
||||||
Cancel actor gracefully once it's "main" portal's
|
Cancel actor gracefully once it's "main" portal's
|
||||||
result arrives.
|
result arrives.
|
||||||
|
|
||||||
Should only be called for actors spawned with `run_in_actor()`.
|
Should only be called for actors spawned with `run_in_actor()`.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
# if this call errors we store the exception for later
|
# if this call errors we store the exception for later
|
||||||
# in ``errors`` which will be reraised inside
|
# in ``errors`` which will be reraised inside
|
||||||
# a MultiError and we still send out a cancel request
|
# a MultiError and we still send out a cancel request
|
||||||
|
@ -175,10 +184,37 @@ async def do_hard_kill(
|
||||||
# XXX: should pretty much never get here unless we have
|
# XXX: should pretty much never get here unless we have
|
||||||
# to move the bits from ``proc.__aexit__()`` out and
|
# to move the bits from ``proc.__aexit__()`` out and
|
||||||
# into here.
|
# into here.
|
||||||
log.critical(f"HARD KILLING {proc}")
|
log.critical(f"#ZOMBIE_LORD_IS_HERE: {proc}")
|
||||||
proc.kill()
|
proc.kill()
|
||||||
|
|
||||||
|
|
||||||
|
async def soft_wait(
|
||||||
|
|
||||||
|
proc: ProcessType,
|
||||||
|
wait_func: Callable[
|
||||||
|
[ProcessType],
|
||||||
|
Awaitable,
|
||||||
|
],
|
||||||
|
portal: Portal,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
# Wait for proc termination but **dont' yet** call
|
||||||
|
# ``trio.Process.__aexit__()`` (it tears down stdio
|
||||||
|
# which will kill any waiting remote pdb trace).
|
||||||
|
# This is a "soft" (cancellable) join/reap.
|
||||||
|
try:
|
||||||
|
await wait_func(proc)
|
||||||
|
except trio.Cancelled:
|
||||||
|
# if cancelled during a soft wait, cancel the child
|
||||||
|
# actor before entering the hard reap sequence
|
||||||
|
# below. This means we try to do a graceful teardown
|
||||||
|
# via sending a cancel message before getting out
|
||||||
|
# zombie killing tools.
|
||||||
|
with trio.CancelScope(shield=True):
|
||||||
|
await portal.cancel_actor()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def new_proc(
|
async def new_proc(
|
||||||
|
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -195,11 +231,14 @@ async def new_proc(
|
||||||
task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED
|
task_status: TaskStatus[Portal] = trio.TASK_STATUS_IGNORED
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
'''
|
||||||
Create a new ``multiprocessing.Process`` using the
|
Create a new ``Process`` using a "spawn method" as (configured using
|
||||||
spawn method as configured using ``try_set_start_method()``.
|
``try_set_start_method()``).
|
||||||
|
|
||||||
"""
|
This routine should be started in a actor runtime task and the logic
|
||||||
|
here is to be considered the core supervision strategy.
|
||||||
|
|
||||||
|
'''
|
||||||
# mark the new actor with the global spawn method
|
# mark the new actor with the global spawn method
|
||||||
subactor._spawn_method = _spawn_method
|
subactor._spawn_method = _spawn_method
|
||||||
uid = subactor.uid
|
uid = subactor.uid
|
||||||
|
@ -230,17 +269,19 @@ async def new_proc(
|
||||||
]
|
]
|
||||||
|
|
||||||
cancelled_during_spawn: bool = False
|
cancelled_during_spawn: bool = False
|
||||||
|
proc: Optional[trio.Process] = None
|
||||||
try:
|
try:
|
||||||
proc = await trio.open_process(spawn_cmd)
|
|
||||||
|
|
||||||
log.runtime(f"Started {proc}")
|
|
||||||
|
|
||||||
# wait for actor to spawn and connect back to us
|
|
||||||
# channel should have handshake completed by the
|
|
||||||
# local actor by the time we get a ref to it
|
|
||||||
try:
|
try:
|
||||||
|
proc = await trio.open_process(spawn_cmd)
|
||||||
|
|
||||||
|
log.runtime(f"Started {proc}")
|
||||||
|
|
||||||
|
# wait for actor to spawn and connect back to us
|
||||||
|
# channel should have handshake completed by the
|
||||||
|
# local actor by the time we get a ref to it
|
||||||
event, chan = await actor_nursery._actor.wait_for_peer(
|
event, chan = await actor_nursery._actor.wait_for_peer(
|
||||||
subactor.uid)
|
subactor.uid)
|
||||||
|
|
||||||
except trio.Cancelled:
|
except trio.Cancelled:
|
||||||
cancelled_during_spawn = True
|
cancelled_during_spawn = True
|
||||||
# we may cancel before the child connects back in which
|
# we may cancel before the child connects back in which
|
||||||
|
@ -250,7 +291,8 @@ async def new_proc(
|
||||||
# don't clobber an ongoing pdb
|
# don't clobber an ongoing pdb
|
||||||
if is_root_process():
|
if is_root_process():
|
||||||
await maybe_wait_for_debugger()
|
await maybe_wait_for_debugger()
|
||||||
else:
|
|
||||||
|
elif proc is not None:
|
||||||
async with acquire_debug_lock(uid):
|
async with acquire_debug_lock(uid):
|
||||||
# soft wait on the proc to terminate
|
# soft wait on the proc to terminate
|
||||||
with trio.move_on_after(0.5):
|
with trio.move_on_after(0.5):
|
||||||
|
@ -291,21 +333,14 @@ async def new_proc(
|
||||||
errors
|
errors
|
||||||
)
|
)
|
||||||
|
|
||||||
# Wait for proc termination but **dont' yet** call
|
# This is a "soft" (cancellable) join/reap which
|
||||||
# ``trio.Process.__aexit__()`` (it tears down stdio
|
# will remote cancel the actor on a ``trio.Cancelled``
|
||||||
# which will kill any waiting remote pdb trace).
|
# condition.
|
||||||
# This is a "soft" (cancellable) join/reap.
|
await soft_wait(
|
||||||
try:
|
proc,
|
||||||
await proc.wait()
|
trio.Process.wait,
|
||||||
except trio.Cancelled:
|
portal
|
||||||
# if cancelled during a soft wait, cancel the child
|
)
|
||||||
# actor before entering the hard reap sequence
|
|
||||||
# below. This means we try to do a graceful teardown
|
|
||||||
# via sending a cancel message before getting out
|
|
||||||
# zombie killing tools.
|
|
||||||
with trio.CancelScope(shield=True):
|
|
||||||
await portal.cancel_actor()
|
|
||||||
raise
|
|
||||||
|
|
||||||
# cancel result waiter that may have been spawned in
|
# cancel result waiter that may have been spawned in
|
||||||
# tandem if not done already
|
# tandem if not done already
|
||||||
|
@ -320,23 +355,26 @@ async def new_proc(
|
||||||
# killing the process too early.
|
# killing the process too early.
|
||||||
log.cancel(f'Hard reap sequence starting for {uid}')
|
log.cancel(f'Hard reap sequence starting for {uid}')
|
||||||
|
|
||||||
with trio.CancelScope(shield=True):
|
if proc:
|
||||||
|
with trio.CancelScope(shield=True):
|
||||||
|
|
||||||
# don't clobber an ongoing pdb
|
# don't clobber an ongoing pdb
|
||||||
if cancelled_during_spawn:
|
if cancelled_during_spawn:
|
||||||
# Try again to avoid TTY clobbering.
|
# Try again to avoid TTY clobbering.
|
||||||
async with acquire_debug_lock(uid):
|
async with acquire_debug_lock(uid):
|
||||||
with trio.move_on_after(0.5):
|
with trio.move_on_after(0.5):
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
|
|
||||||
if is_root_process():
|
if is_root_process():
|
||||||
await maybe_wait_for_debugger()
|
await maybe_wait_for_debugger()
|
||||||
|
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
log.cancel(f"Attempting to hard kill {proc}")
|
log.cancel(f"Attempting to hard kill {proc}")
|
||||||
await do_hard_kill(proc)
|
await do_hard_kill(proc)
|
||||||
|
|
||||||
log.debug(f"Joined {proc}")
|
log.debug(f"Joined {proc}")
|
||||||
|
else:
|
||||||
|
log.warning('Nursery cancelled before sub-proc started')
|
||||||
|
|
||||||
if not cancelled_during_spawn:
|
if not cancelled_during_spawn:
|
||||||
# pop child entry to indicate we no longer managing this
|
# pop child entry to indicate we no longer managing this
|
||||||
|
@ -351,6 +389,7 @@ async def new_proc(
|
||||||
actor_nursery=actor_nursery,
|
actor_nursery=actor_nursery,
|
||||||
subactor=subactor,
|
subactor=subactor,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
|
|
||||||
# passed through to actor main
|
# passed through to actor main
|
||||||
bind_addr=bind_addr,
|
bind_addr=bind_addr,
|
||||||
parent_addr=parent_addr,
|
parent_addr=parent_addr,
|
||||||
|
@ -469,7 +508,14 @@ async def mp_new_proc(
|
||||||
errors
|
errors
|
||||||
)
|
)
|
||||||
|
|
||||||
await proc_waiter(proc)
|
# This is a "soft" (cancellable) join/reap which
|
||||||
|
# will remote cancel the actor on a ``trio.Cancelled``
|
||||||
|
# condition.
|
||||||
|
await soft_wait(
|
||||||
|
proc,
|
||||||
|
proc_waiter,
|
||||||
|
portal
|
||||||
|
)
|
||||||
|
|
||||||
# cancel result waiter that may have been spawned in
|
# cancel result waiter that may have been spawned in
|
||||||
# tandem if not done already
|
# tandem if not done already
|
||||||
|
|
|
@ -52,6 +52,7 @@ class ActorNursery:
|
||||||
self.cancelled: bool = False
|
self.cancelled: bool = False
|
||||||
self._join_procs = trio.Event()
|
self._join_procs = trio.Event()
|
||||||
self.errors = errors
|
self.errors = errors
|
||||||
|
self.exited = trio.Event()
|
||||||
|
|
||||||
async def start_actor(
|
async def start_actor(
|
||||||
self,
|
self,
|
||||||
|
@ -207,7 +208,8 @@ class ActorNursery:
|
||||||
|
|
||||||
# spawn cancel tasks for each sub-actor
|
# spawn cancel tasks for each sub-actor
|
||||||
assert portal
|
assert portal
|
||||||
nursery.start_soon(portal.cancel_actor)
|
if portal.channel.connected():
|
||||||
|
nursery.start_soon(portal.cancel_actor)
|
||||||
|
|
||||||
# if we cancelled the cancel (we hung cancelling remote actors)
|
# if we cancelled the cancel (we hung cancelling remote actors)
|
||||||
# then hard kill all sub-processes
|
# then hard kill all sub-processes
|
||||||
|
@ -401,18 +403,23 @@ async def open_nursery(
|
||||||
async with open_root_actor(**kwargs) as actor:
|
async with open_root_actor(**kwargs) as actor:
|
||||||
assert actor is current_actor()
|
assert actor is current_actor()
|
||||||
|
|
||||||
# try:
|
try:
|
||||||
|
async with _open_and_supervise_one_cancels_all_nursery(
|
||||||
|
actor
|
||||||
|
) as anursery:
|
||||||
|
yield anursery
|
||||||
|
finally:
|
||||||
|
anursery.exited.set()
|
||||||
|
|
||||||
|
else: # sub-nursery case
|
||||||
|
|
||||||
|
try:
|
||||||
async with _open_and_supervise_one_cancels_all_nursery(
|
async with _open_and_supervise_one_cancels_all_nursery(
|
||||||
actor
|
actor
|
||||||
) as anursery:
|
) as anursery:
|
||||||
yield anursery
|
yield anursery
|
||||||
|
finally:
|
||||||
else: # sub-nursery case
|
anursery.exited.set()
|
||||||
|
|
||||||
async with _open_and_supervise_one_cancels_all_nursery(
|
|
||||||
actor
|
|
||||||
) as anursery:
|
|
||||||
yield anursery
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
log.debug("Nursery teardown complete")
|
log.debug("Nursery teardown complete")
|
||||||
|
|
Loading…
Reference in New Issue