Merge pull request #264 from goodboy/runinactor_none_result
Fix `Portal.run_in_actor()` returns `None` resultfaster_daemon_cancels
commit
ac821bdd94
|
@ -0,0 +1,8 @@
|
||||||
|
Fix ``Portal.run_in_actor()`` returns ``None`` result.
|
||||||
|
|
||||||
|
``None`` was being used as the cached result flag and obviously breaks
|
||||||
|
on a ``None`` returned from the remote target task. This would cause an
|
||||||
|
infinite hang if user code ever called ``Portal.result()`` *before* the
|
||||||
|
nursery exit. The simple fix is to use the *return message* as the
|
||||||
|
initial "no-result-received-yet" flag value and, once received, the
|
||||||
|
return value is read from the message to avoid the cache logic error.
|
|
@ -1,7 +1,7 @@
|
||||||
"""
|
"""
|
||||||
Spawning basics
|
Spawning basics
|
||||||
"""
|
"""
|
||||||
from typing import Dict, Tuple
|
from typing import Dict, Tuple, Optional
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
|
@ -93,24 +93,38 @@ async def test_movie_theatre_convo(start_method):
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
||||||
async def cellar_door():
|
async def cellar_door(return_value: Optional[str]):
|
||||||
return "Dang that's beautiful"
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'return_value', ["Dang that's beautiful", None],
|
||||||
|
ids=['return_str', 'return_None'],
|
||||||
|
)
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_most_beautiful_word(start_method):
|
async def test_most_beautiful_word(
|
||||||
"""The main ``tractor`` routine.
|
start_method,
|
||||||
"""
|
return_value
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
The main ``tractor`` routine.
|
||||||
|
|
||||||
|
'''
|
||||||
|
with trio.fail_after(1):
|
||||||
async with tractor.open_nursery() as n:
|
async with tractor.open_nursery() as n:
|
||||||
|
|
||||||
portal = await n.run_in_actor(
|
portal = await n.run_in_actor(
|
||||||
cellar_door,
|
cellar_door,
|
||||||
|
return_value=return_value,
|
||||||
name='some_linguist',
|
name='some_linguist',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
print(await portal.result())
|
||||||
# The ``async with`` will unblock here since the 'some_linguist'
|
# The ``async with`` will unblock here since the 'some_linguist'
|
||||||
# actor has completed its main task ``cellar_door``.
|
# actor has completed its main task ``cellar_door``.
|
||||||
|
|
||||||
|
# this should pull the cached final result already captured during
|
||||||
|
# the nursery block exit.
|
||||||
print(await portal.result())
|
print(await portal.result())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ from .log import get_logger
|
||||||
from ._exceptions import (
|
from ._exceptions import (
|
||||||
unpack_error,
|
unpack_error,
|
||||||
NoResult,
|
NoResult,
|
||||||
RemoteActorError,
|
# RemoteActorError,
|
||||||
ContextCancelled,
|
ContextCancelled,
|
||||||
)
|
)
|
||||||
from ._streaming import Context, ReceiveMsgStream
|
from ._streaming import Context, ReceiveMsgStream
|
||||||
|
@ -54,8 +54,23 @@ def func_deats(func: Callable) -> Tuple[str, str]:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _unwrap_msg(
|
||||||
|
|
||||||
|
msg: dict[str, Any],
|
||||||
|
channel: Channel
|
||||||
|
|
||||||
|
) -> Any:
|
||||||
|
try:
|
||||||
|
return msg['return']
|
||||||
|
except KeyError:
|
||||||
|
# internal error should never get here
|
||||||
|
assert msg.get('cid'), "Received internal error at portal?"
|
||||||
|
raise unpack_error(msg, channel)
|
||||||
|
|
||||||
|
|
||||||
class Portal:
|
class Portal:
|
||||||
"""A 'portal' to a(n) (remote) ``Actor``.
|
'''
|
||||||
|
A 'portal' to a(n) (remote) ``Actor``.
|
||||||
|
|
||||||
A portal is "opened" (and eventually closed) by one side of an
|
A portal is "opened" (and eventually closed) by one side of an
|
||||||
inter-actor communication context. The side which opens the portal
|
inter-actor communication context. The side which opens the portal
|
||||||
|
@ -71,13 +86,14 @@ class Portal:
|
||||||
function calling semantics are supported transparently; hence it is
|
function calling semantics are supported transparently; hence it is
|
||||||
like having a "portal" between the seperate actor memory spaces.
|
like having a "portal" between the seperate actor memory spaces.
|
||||||
|
|
||||||
"""
|
'''
|
||||||
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
|
# when this is set to a tuple returned from ``_submit()`` then
|
||||||
# it is expected that ``result()`` will be awaited at some point
|
# it is expected that ``result()`` will be awaited at some point
|
||||||
# during the portal's lifetime
|
# during the portal's lifetime
|
||||||
self._result: Optional[Any] = None
|
self._result_msg: Optional[dict] = None
|
||||||
|
|
||||||
# set when _submit_for_result is called
|
# 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]]
|
||||||
|
@ -128,16 +144,12 @@ class Portal:
|
||||||
recv_chan: trio.abc.ReceiveChannel,
|
recv_chan: trio.abc.ReceiveChannel,
|
||||||
resptype: str,
|
resptype: str,
|
||||||
first_msg: dict
|
first_msg: dict
|
||||||
) -> Any:
|
|
||||||
assert resptype == 'asyncfunc' # single response
|
|
||||||
|
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
|
||||||
|
assert resptype == 'asyncfunc' # single response
|
||||||
msg = await recv_chan.receive()
|
msg = await recv_chan.receive()
|
||||||
try:
|
return msg
|
||||||
return msg['return']
|
|
||||||
except KeyError:
|
|
||||||
# internal error should never get here
|
|
||||||
assert msg.get('cid'), "Received internal error at portal?"
|
|
||||||
raise unpack_error(msg, self.channel)
|
|
||||||
|
|
||||||
async def result(self) -> Any:
|
async def result(self) -> Any:
|
||||||
"""Return the result(s) from the remote actor's "main" task.
|
"""Return the result(s) from the remote actor's "main" task.
|
||||||
|
@ -158,17 +170,11 @@ class Portal:
|
||||||
|
|
||||||
# expecting a "main" result
|
# expecting a "main" result
|
||||||
assert self._expect_result
|
assert self._expect_result
|
||||||
if self._result is None:
|
|
||||||
try:
|
|
||||||
self._result = await self._return_once(*self._expect_result)
|
|
||||||
except RemoteActorError as err:
|
|
||||||
self._result = err
|
|
||||||
|
|
||||||
# re-raise error on every call
|
if self._result_msg is None:
|
||||||
if isinstance(self._result, RemoteActorError):
|
self._result_msg = await self._return_once(*self._expect_result)
|
||||||
raise self._result
|
|
||||||
|
|
||||||
return self._result
|
return _unwrap_msg(self._result_msg, self.channel)
|
||||||
|
|
||||||
async def _cancel_streams(self):
|
async def _cancel_streams(self):
|
||||||
# terminate all locally running async generator
|
# terminate all locally running async generator
|
||||||
|
@ -243,9 +249,10 @@ class Portal:
|
||||||
instance methods in the remote runtime. Currently this should only
|
instance methods in the remote runtime. Currently this should only
|
||||||
be used for `tractor` internals.
|
be used for `tractor` internals.
|
||||||
"""
|
"""
|
||||||
return 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))
|
||||||
)
|
)
|
||||||
|
return _unwrap_msg(msg, self.channel)
|
||||||
|
|
||||||
async def run(
|
async def run(
|
||||||
self,
|
self,
|
||||||
|
@ -253,12 +260,14 @@ class Portal:
|
||||||
fn_name: Optional[str] = None,
|
fn_name: Optional[str] = None,
|
||||||
**kwargs
|
**kwargs
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Submit a remote function to be scheduled and run by actor, in
|
'''
|
||||||
|
Submit a remote function to be scheduled and run by actor, in
|
||||||
a new task, wrap and return its (stream of) result(s).
|
a new task, wrap and return its (stream of) result(s).
|
||||||
|
|
||||||
This is a blocking call and returns either a value from the
|
This is a blocking call and returns either a value from the
|
||||||
remote rpc task or a local async generator instance.
|
remote rpc task or a local async generator instance.
|
||||||
"""
|
|
||||||
|
'''
|
||||||
if isinstance(func, str):
|
if isinstance(func, str):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"`Portal.run(namespace: str, funcname: str)` is now"
|
"`Portal.run(namespace: str, funcname: str)` is now"
|
||||||
|
@ -284,8 +293,11 @@ class Portal:
|
||||||
|
|
||||||
fn_mod_path, fn_name = func_deats(func)
|
fn_mod_path, fn_name = func_deats(func)
|
||||||
|
|
||||||
return await self._return_once(
|
return _unwrap_msg(
|
||||||
*(await self._submit(fn_mod_path, fn_name, kwargs))
|
await self._return_once(
|
||||||
|
*(await self._submit(fn_mod_path, fn_name, kwargs)),
|
||||||
|
),
|
||||||
|
self.channel,
|
||||||
)
|
)
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|
Loading…
Reference in New Issue