345 lines
8.4 KiB
Python
345 lines
8.4 KiB
Python
'''
|
|
`tractor.to_actor`: one-shot single-remote-task API suite.
|
|
|
|
Verifies the "spiritual successor" to (and eventual
|
|
replacement of) `ActorNursery.run_in_actor()`; see
|
|
https://github.com/goodboy/tractor/issues/477
|
|
|
|
'''
|
|
from functools import partial
|
|
|
|
import pytest
|
|
import trio
|
|
import tractor
|
|
from tractor import (
|
|
RemoteActorError,
|
|
to_actor,
|
|
)
|
|
from tractor._testing import tractor_test
|
|
|
|
|
|
async def add_one(
|
|
n: int,
|
|
) -> int:
|
|
return n + 1
|
|
|
|
|
|
async def raise_value_error() -> None:
|
|
raise ValueError('kaboom')
|
|
|
|
|
|
@tractor_test
|
|
async def test_one_shot_in_private_nursery(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
No `an`/`portal` provided: a private actor-nursery
|
|
is opened (and torn down) scoped to just the call.
|
|
|
|
'''
|
|
assert await to_actor.run(
|
|
add_one,
|
|
n=1,
|
|
) == 2
|
|
|
|
|
|
def test_one_shot_boots_implicit_runtime(
|
|
reg_addr: tuple,
|
|
start_method: str,
|
|
loglevel: str,
|
|
):
|
|
'''
|
|
Outside any actor-runtime `to_actor.run()` boots one
|
|
implicitly (just like bare `open_nursery()` usage)
|
|
configured via pass-through `runtime_kwargs`.
|
|
|
|
'''
|
|
async def main() -> None:
|
|
assert tractor.current_actor(
|
|
err_on_no_runtime=False,
|
|
) is None
|
|
result = await to_actor.run(
|
|
add_one,
|
|
n=41,
|
|
runtime_kwargs=dict(
|
|
registry_addrs=[reg_addr],
|
|
start_method=start_method,
|
|
loglevel=loglevel,
|
|
),
|
|
)
|
|
assert result == 42
|
|
|
|
trio.run(main)
|
|
|
|
|
|
@tractor_test
|
|
async def test_remote_error_relayed_to_caller_task(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
A remote task error is raised directly in the
|
|
caller's task as a boxed `RemoteActorError` instead
|
|
of surfacing at actor-nursery teardown as with the
|
|
legacy `.run_in_actor()` API.
|
|
|
|
'''
|
|
with pytest.raises(RemoteActorError) as excinfo:
|
|
await to_actor.run(raise_value_error)
|
|
|
|
assert excinfo.value.boxed_type is ValueError
|
|
|
|
|
|
@tractor_test
|
|
async def test_spawn_from_caller_nursery(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
Pass a caller-managed `an: ActorNursery` for the spawn.
|
|
|
|
Previously `to_actor.run()` treated an actor-runtime cancel ack
|
|
as process reaping, so the call returned while the child monitor
|
|
and its `ActorNursery._children` record remained alive until the
|
|
entire nursery exited. The assertion inside the still-open
|
|
nursery proves child-process joining and record removal now
|
|
complete before the one-shot call returns.
|
|
|
|
'''
|
|
async with tractor.open_nursery() as an:
|
|
assert await to_actor.run(
|
|
add_one,
|
|
an=an,
|
|
n=10,
|
|
) == 11
|
|
assert not an._children
|
|
|
|
|
|
@tractor_test
|
|
async def test_cancel_ack_failure_hard_reaps_child(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
Escalate a failed cancel acknowledgement and reap the child.
|
|
|
|
`Portal.cancel_actor()` can return `False` when its transport is
|
|
already closed without confirming runtime cancellation. The old
|
|
one-shot path ignored that result, released the nursery-wide join
|
|
gate and then waited forever for a still-running process. This
|
|
test forces that exact result without cancelling the actor, caps
|
|
the call to detect the former hang and verifies the child monitor
|
|
removes its `ActorNursery._children` record before returning.
|
|
|
|
'''
|
|
async def cancel_without_ack(
|
|
portal: tractor.Portal,
|
|
timeout: float|None = None,
|
|
raise_on_timeout: bool = False,
|
|
) -> bool:
|
|
assert raise_on_timeout
|
|
return False
|
|
|
|
monkeypatch.setattr(
|
|
tractor.Portal,
|
|
'cancel_actor',
|
|
cancel_without_ack,
|
|
)
|
|
|
|
async with tractor.open_nursery() as an:
|
|
with trio.fail_after(5):
|
|
assert await to_actor.run(
|
|
add_one,
|
|
an=an,
|
|
n=20,
|
|
) == 21
|
|
assert not an._children
|
|
|
|
|
|
def test_late_child_reap_registration_is_released():
|
|
'''
|
|
Preserve a nursery-wide reap request across child startup.
|
|
|
|
A child monitor can checkpoint while connecting to its parent as
|
|
the surrounding `ActorNursery` begins teardown. Previously the
|
|
nursery signalled only already-registered child events, so a
|
|
monitor registering afterward waited forever. This models that
|
|
ordering by publishing the nursery-wide request first and proves
|
|
the later per-child event inherits its set state immediately.
|
|
|
|
'''
|
|
an = object.__new__(tractor.ActorNursery)
|
|
an._join_procs = trio.Event()
|
|
an._child_reap_requests = {}
|
|
an._child_reaped = {}
|
|
|
|
an._join_procs.set()
|
|
reap_request, _ = an._register_child_reap(
|
|
('late_child', 'uid'),
|
|
)
|
|
|
|
assert reap_request.is_set()
|
|
|
|
|
|
@tractor_test
|
|
async def test_remote_error_from_caller_nursery(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
With a caller-managed `an` the remote error also
|
|
surfaces in the caller's task, INSIDE the nursery
|
|
block, allowing inline (supervision-style) handling.
|
|
|
|
'''
|
|
async with tractor.open_nursery() as an:
|
|
with pytest.raises(RemoteActorError) as excinfo:
|
|
await to_actor.run(
|
|
raise_value_error,
|
|
an=an,
|
|
)
|
|
|
|
assert excinfo.value.boxed_type is ValueError
|
|
|
|
|
|
@tractor_test
|
|
async def test_reuse_existing_actor_via_portal(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
Pass `portal=` to schedule the one-shot task in an
|
|
already-running actor; no spawn, no implicit reap.
|
|
|
|
'''
|
|
async with tractor.open_nursery() as an:
|
|
portal: tractor.Portal = await an.start_actor(
|
|
'one_shot_worker',
|
|
enable_modules=[__name__],
|
|
)
|
|
for i in range(3):
|
|
assert await to_actor.run(
|
|
add_one,
|
|
portal=portal,
|
|
n=i,
|
|
) == i + 1
|
|
|
|
# still alive: caller owns the actor's lifetime.
|
|
await portal.cancel_actor()
|
|
|
|
|
|
@tractor_test
|
|
async def test_concurrent_one_shots_from_task_nursery(
|
|
start_method: str,
|
|
debug_mode: bool,
|
|
):
|
|
'''
|
|
The worker-pool-ish pattern from #477: concurrency
|
|
is composed with a plain (caller-side) `trio` task
|
|
nursery scheduling multiple one-shot calls against
|
|
a shared caller-managed actor-nursery; error
|
|
collection thus lives entirely in caller-code.
|
|
|
|
'''
|
|
results: dict[int, int] = {}
|
|
|
|
async def one_shot(
|
|
an: tractor.ActorNursery,
|
|
i: int,
|
|
) -> None:
|
|
results[i] = await to_actor.run(
|
|
add_one,
|
|
an=an,
|
|
name=f'one_shot_{i}',
|
|
n=i,
|
|
)
|
|
|
|
async with (
|
|
tractor.open_nursery() as an,
|
|
trio.open_nursery() as tn,
|
|
):
|
|
for i in range(4):
|
|
tn.start_soon(one_shot, an, i)
|
|
|
|
assert results == {
|
|
i: i + 1 for i in range(4)
|
|
}
|
|
|
|
|
|
def test_rejects_sync_fn():
|
|
'''
|
|
Non-async callables error BEFORE any spawn (or even
|
|
runtime-boot) happens.
|
|
|
|
'''
|
|
def not_async() -> None:
|
|
...
|
|
|
|
with pytest.raises(TypeError):
|
|
trio.run(
|
|
partial(
|
|
to_actor.run,
|
|
not_async,
|
|
)
|
|
)
|
|
|
|
|
|
def test_rejects_streaming_fn():
|
|
'''
|
|
Async-gen (streaming) fns are not one-shot-able,
|
|
same constraint as `Portal.run()`.
|
|
|
|
'''
|
|
async def agen():
|
|
yield 1
|
|
|
|
with pytest.raises(TypeError):
|
|
trio.run(
|
|
partial(
|
|
to_actor.run,
|
|
agen,
|
|
)
|
|
)
|
|
|
|
|
|
def test_rejects_portal_and_an_combo():
|
|
'''
|
|
`portal=` and `an=` are mutually exclusive
|
|
placement options.
|
|
|
|
'''
|
|
with pytest.raises(ValueError):
|
|
trio.run(
|
|
partial(
|
|
to_actor.run,
|
|
add_one,
|
|
portal=object(),
|
|
an=object(),
|
|
n=1,
|
|
)
|
|
)
|
|
|
|
|
|
def test_rejects_runtime_kwargs_with_placement():
|
|
'''
|
|
`runtime_kwargs` only applies when the call opens
|
|
its own private actor-nursery; passing it alongside
|
|
a placement opt is an error, never silently
|
|
ignored.
|
|
|
|
'''
|
|
with pytest.raises(ValueError):
|
|
trio.run(
|
|
partial(
|
|
to_actor.run,
|
|
add_one,
|
|
an=object(),
|
|
runtime_kwargs=dict(
|
|
loglevel='cancel',
|
|
),
|
|
n=1,
|
|
)
|
|
)
|