asyncio interop: tractor.to_asyncio#

“Infected asyncio” mode: spawn an actor with start_actor(..., infect_asyncio=True) and its process runs trio as a guest on top of the asyncio loop — letting your trio task tree drive asyncio tasks in the same process while the rest of the actor tree stays pure trio. Each trio <-> asyncio task pair is linked with structured concurrency (SC) semantics: error or cancellation on either side tears down both, with the cause translated cross-loop.

trio guest mode inside an asyncio-infected actor

A trio guest driving asyncio tasks in one actor.#

See examples/infected_asyncio_echo_server.py for a complete worked example.

Starting asyncio tasks from trio#

tractor.to_asyncio.open_channel_from(target, suppress_graceful_exits=True, **target_kwargs)[source]#

Start an asyncio.Task as target() and open an inter-loop (linked) channel for streaming between it and the current trio.Task.

A pair (chan: LinkedTaskChannel, Any) is delivered to the caller where the 2nd element is the value provided by the asyncio.Task’s unblocking call to chan.started_nowait().

Parameters:
Return type:

AsyncIterator[tuple[LinkedTaskChannel, Any]]

async tractor.to_asyncio.run_task(func, *, qsize=1024, **kwargs)[source]#

Run an asyncio-compat async function or generator in a task, return or stream the result back to trio.

Parameters:
Return type:

Any

Note

open_channel_from() mirrors the Portal.open_context() handshake: the asyncio side calls chan.started_nowait(value) and that value pops out as first on the trio side. run_task() is the one-shot form — run a single asyncio-compatible coroutine fn and return its result to trio.

The inter-loop channel#

class tractor.to_asyncio.LinkedTaskChannel(_to_aio, _from_aio, _to_trio, _trio_cs, _trio_task, _aio_task_complete, _closed_by_aio_task=False, _suppress_graceful_exits=True, _trio_err=None, _trio_to_raise=None, _trio_exited=False, _aio_task=None, _aio_err=None, _aio_to_raise=None, _aio_result=<class 'tractor._context.Unresolved'>, _broadcaster=None)[source]#

A “linked task channel” which allows for two-way synchronized msg passing between a trio-in-guest-mode task and an asyncio task scheduled in the host loop.

Parameters:
async wait_for_result(hide_tb=True)[source]#

Wait for the asyncio.Task.result() from trio

Parameters:

hide_tb (bool)

Return type:

Any

started_nowait(val=None)[source]#

Synchronize aio-side with its trio-parent.

Parameters:

val (Any)

Return type:

None

async receive()[source]#

Receive a value trio.Task <- asyncio.Task.

Note the tasks in each loop are “SC linked” as a pair with exception/cancel handling to teardown both sides on any unexpected error or cancellation.

Return type:

Any

async get()[source]#

Receive a value asyncio.Task <- trio.Task.

This is equiv to await self._to_aio.get().

Return type:

Any

async send(item)[source]#

Send a value trio.Task -> asyncio.Task by enqueuing item onto the internal asyncio.Queue via put_nowait().

Parameters:

item (Any)

Return type:

None

send_nowait(item)[source]#

Send a value through FROM the asyncio.Task to the trio.Task NON-BLOCKING.

This is equiv to self._to_trio.send_nowait().

Parameters:

item (Any)

Return type:

None

subscribe()[source]#

Allocate and return a BroadcastReceiver which delegates to this inter-task channel.

This allows multiple local tasks to receive each their own copy of this message stream.

See tractor._streaming.MsgStream.subscribe() for further similar details.

Return type:

AsyncIterator[BroadcastReceiver]

Note

The trio side uses the async API (LinkedTaskChannel.send() / LinkedTaskChannel.receive()); the asyncio side uses the loop-safe sync/await mix (LinkedTaskChannel.send_nowait() / LinkedTaskChannel.get() / LinkedTaskChannel.started_nowait()).

Translated exception types#

Cross-loop failures are re-raised on the other side as one of these explicit translation types, so you always know which loop actually died first:

exception tractor._exceptions.AsyncioCancelled[source]#

Asyncio cancelled translation (non-base) error for use with the to_asyncio module to be raised in the trio side task

NOTE: this should NOT inherit from asyncio.CancelledError or tests should break!

exception tractor._exceptions.AsyncioTaskExited[source]#

asyncio.Task “exited” translation error for use with the to_asyncio APIs to be raised in the trio side task indicating on .run_task()/.open_channel_from() exit that the aio side exited early/silently.

exception tractor._exceptions.TrioCancelled[source]#

Trio cancelled translation (non-base) error for use with the to_asyncio module to be raised in the asyncio.Task to indicate that the trio side raised Cancelled or an error.

exception tractor._exceptions.TrioTaskExited[source]#

The trio-side task exited without explicitly cancelling the asyncio.Task peer.

This is very similar to how trio.ClosedResource acts as a “clean shutdown” signal to the consumer side of a mem-chan,

https://trio.readthedocs.io/en/stable/reference-core.html#clean-shutdown-with-channels

exception tractor.to_asyncio.AsyncioRuntimeTranslationError[source]#

Bases: RuntimeError

We failed to correctly relay runtime semantics and/or maintain SC supervision rules cross-event-loop.

Guest-mode entrypoint#

run_as_asyncio_guest() is the runtime-internal entrypoint that boots trio in guest mode inside an infected actor — you get it implicitly via infect_asyncio=True and shouldn’t need to call it yourself.

See also

Runtime and spawning for the infect_asyncio spawn flag, tractor.Actor.is_infected_aio() for runtime introspection, Debugging and devx: tractor.devx for using the debugger from inside asyncio tasks, and Infected asyncio for the guided tour.