2026-07-02 16:10:23 +00:00
|
|
|
'''
|
|
|
|
|
`tractor.to_actor`: one-shot single-remote-task API suite.
|
|
|
|
|
|
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
|
|
|
Verifies the "spiritual successor" to (and replacement of)
|
|
|
|
|
the removed legacy `ActorNursery.run_in_actor()`; see
|
2026-07-02 16:10:23 +00:00
|
|
|
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
|
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
|
|
|
removed legacy `.run_in_actor()` API.
|
2026-07-02 16:10:23 +00:00
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
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; the subactor is still one-shot reaped by the
|
|
|
|
|
time the call returns.
|
|
|
|
|
|
|
|
|
|
'''
|
|
|
|
|
async with tractor.open_nursery() as an:
|
|
|
|
|
assert await to_actor.run(
|
|
|
|
|
add_one,
|
|
|
|
|
an=an,
|
|
|
|
|
n=10,
|
|
|
|
|
) == 11
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@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,
|
|
|
|
|
)
|
|
|
|
|
)
|