Skip to content

Extend the to_asyncio inter-loop-task channel iface#413

Open
goodboy wants to merge 4 commits intomainfrom
to_asyncio_channel_iface
Open

Extend the to_asyncio inter-loop-task channel iface#413
goodboy wants to merge 4 commits intomainfrom
to_asyncio_channel_iface

Conversation

@goodboy
Copy link
Owner

@goodboy goodboy commented Feb 19, 2026

Summary

see the description below from copilot.


Todo before merge

  • (1ad2c28) adjust/add tests which use the new API,

    • (some) suites in test_infected_asyncio.py
  • (1ad2c28) adjustments to any examples scripts,

    • debugging/asyncio_bp.py
    • infected_asyncio_echo_server.py
  • adjust any docs?


In follow up?

  • change the order to (chan, first) like we deliver from
    Portal.open_context()?
    • do it now or as a follow up breaking change?

@goodboy goodboy requested review from Copilot and removed request for Copilot February 19, 2026 21:47
@goodboy goodboy added enhancement New feature or request api streaming supervision asyncio ur non-favorite stdlib module labels Feb 19, 2026
Copilot AI review requested due to automatic review settings March 5, 2026 02:39
@goodboy goodboy force-pushed the to_asyncio_channel_iface branch from 2616f4b to ec65521 Compare March 5, 2026 02:39
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Extends tractor.to_asyncio’s inter-event-loop linked-task channel so asyncio tasks can interact via a single LinkedTaskChannel object (instead of separate to_trio/from_trio params), and updates tests to use the new API.

Changes:

  • Switch debug import usage to tractor.devx and route debug helpers through devx.debug.
  • Extend LinkedTaskChannel with get() (asyncio receives from trio) and a new non-blocking send helper (asyncio sends to trio).
  • Update infected-asyncio echo server test to accept a LinkedTaskChannel directly and use the new methods.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
tractor/to_asyncio.py Adds/updates LinkedTaskChannel methods, adjusts debug plumbing, and updates open_channel_from() typing/docs.
tests/test_infected_asyncio.py Refactors the asyncio echo-server test to use the channel object directly (chan.get(), chan.send_nowait(), chan.started_nowait()).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +282 to 287
Send a value through `trio.Task` -> `asyncio.Task`
presuming
it defines a `from_trio` argument or makes calls
to `chan.get()` , if it does not
this method will raise an error.

Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedTaskChannel.send() docstring still says it will raise an error if the asyncio task doesn't accept from_trio / use chan.get(), but the implementation unconditionally does _to_aio.put_nowait(item) and doesn't validate the target signature or consumer presence. Either adjust the docstring to reflect actual behavior (possible QueueFull if backpressured) or add explicit validation earlier (e.g. in _run_asyncio_task).

Suggested change
Send a value through `trio.Task` -> `asyncio.Task`
presuming
it defines a `from_trio` argument or makes calls
to `chan.get()` , if it does not
this method will raise an error.
Send a value from the `trio.Task` to the `asyncio.Task`.
This enqueues ``item`` into the internal asyncio-side queue
using ``put_nowait``. It does not validate that the target
asyncio task accepts a ``from_trio`` argument or calls
``chan.get()``; correct consumption of items is the caller's
responsibility.
This call is non-blocking and may raise ``asyncio.QueueFull``
if the underlying queue is backpressured.

Copilot uses AI. Check for mistakes.
Comment on lines +1303 to +1313
) -> AsyncIterator[
tuple[LinkedTaskChannel, Any]
]:
'''
Open an inter-loop linked task channel for streaming between a target
spawned ``asyncio`` task and ``trio``.
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()`.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open_channel_from()’s return type annotation and docstring say the context manager yields (LinkedTaskChannel, Any) / “(chan, Any)”, but the implementation yields yield first, chan and all call sites unpack (first, chan). Align the type hint + docstring with the actual yielded tuple order (or swap the yield order if you intend (chan, first), but that would be a breaking API change).

Copilot uses AI. Check for mistakes.
Comment on lines +299 to +304
Send a value through FROM the `asyncio.Task` to
the `trio.Task` NON-BLOCKING.

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

'''
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedTaskChannel.send_nowait() is introduced with reversed direction compared to send()/receive(): send() sends trio -> asyncio (queue put), but send_nowait() sends asyncio -> trio (_to_trio.send_nowait). Because LinkedTaskChannel subclasses trio.abc.Channel, callers will reasonably expect send_nowait to be the non-blocking variant of send (same direction). As-is, trio-side code can accidentally call send_nowait() and silently send to the wrong endpoint (or even back into its own receive stream), causing very hard-to-debug behavior. Consider renaming this to an explicit direction (e.g. send_to_trio_nowait) and/or implementing send_nowait to match send semantics (with a separate method for aio->trio).

Suggested change
Send a value through FROM the `asyncio.Task` to
the `trio.Task` NON-BLOCKING.
This is equiv to `self._to_trio.send_nowait()`.
'''
Send a value through `trio.Task` -> `asyncio.Task`
NON-BLOCKING.
This is the non-blocking variant of :meth:`send` and is
equivalent to calling ``self._to_aio.put_nowait(item)``.
'''
self._to_aio.put_nowait(item)
def send_to_trio_nowait(
self,
item: Any,
) -> None:
'''
Send a value FROM the `asyncio.Task` to the `trio.Task`
NON-BLOCKING.
This is equivalent to ``self._to_trio.send_nowait(item)``.
'''

Copilot uses AI. Check for mistakes.
Comment on lines +275 to +277
This is equiv to `await self._from_trio.get()`.

'''
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring for LinkedTaskChannel.get() references self._from_trio, but this attribute doesn't exist on LinkedTaskChannel (the asyncio-side receive queue is _to_aio). This looks like a stale name and will mislead users reading the API docs; update the wording to match the actual field/method (_to_aio.get() / “asyncio receives from trio via the queue”).

Suggested change
This is equiv to `await self._from_trio.get()`.
'''
This is equivalent to `await self._to_aio.get()`, i.e. asyncio
receives from trio via the internal queue.

Copilot uses AI. Check for mistakes.
@goodboy
Copy link
Owner Author

goodboy commented Mar 8, 2026

Obviously needs (some) tests adjusted to the new API(s).

@goodboy goodboy force-pushed the to_asyncio_channel_iface branch from ec65521 to 3a7be32 Compare March 8, 2026 23:15
goodboy and others added 3 commits March 10, 2026 00:32
With methods to comms similar to those that exist for the `trio` side,
- `.get()` which proxies verbatim to the `._to_aio: asyncio.Queue`,
- `.send_nowait()` which thin-wraps to `._to_trio: trio.MemorySendChannel`.

Obviously the more correct design is to break up the channel type into
a pair of handle types, one for each "side's" task in each event-loop,
that's hopefully coming shortly in a follow up patch B)

Also,
- fill in some missing doc strings, tweak some explanation comments and
  update todos.
- adjust the `test_aio_errors_and_channel_propagates_and_closes()` suite
  to use the new `chan` fn-sig-API with `.open_channel_from()` including
  the new methods for msg comms; ensures everything added here works e2e.
This change is masked out now BUT i'm leaving it in for reference.

I was debugging a multi-actor fault where the primary source actor was
an infected-aio-subactor (`brokerd.ib`) and it seemed like the REPL was only
entering on the `trio` side (at a `.open_channel_from()`) and not
eventually breaking in the `asyncio.Task`. But, since (changing
something?) it seems to be working now, it's just that the `trio` side
seems to sometimes handle before the (source/causing and more
child-ish) `asyncio`-task, which is a bit odd and not expected..
We could likely refine (maybe with an inter-loop-task REPL lock?) this
at some point and ensure a child-`asyncio` task which errors always
grabs the REPL **first**?

Lowlevel deats/further-todos,
- add (masked) `maybe_open_crash_handler()` block around
  `asyncio.Task` execution with notes about weird parent-addr
  delivery bug in `test_sync_pause_from_aio_task`
  * yeah dunno what that's about but made a bug; seems to be IPC
    serialization of the `TCPAddress` struct somewhere??
- add inter-loop lock TODO for avoiding aio-task clobbering
  trio-tasks when both crash in debug-mode

Also,
- change import from `tractor.devx.debug` to `tractor.devx`
- adjust `get_logger()` call to use new implicit mod-name detection
  added to `.log.get_logger()`, i.e. sin `name=__name__`.
- some teensie refinements to `open_channel_from()`:
  * swap return type annotation for  to `tuple[LinkedTaskChannel, Any]`
    (was `Any`).
  * update doc-string to clarify started-value delivery
  * add err-log before `.pause()` in what should be an unreachable path.
  * add todo to swap the `(first, chan)` pair to match that of ctx..

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])

[claude-code-gh]: https://github.com/anthropics/claude-code
@goodboy goodboy force-pushed the to_asyncio_channel_iface branch from 3a7be32 to 7f499cf Compare March 10, 2026 04:32
Convert every remaining `to_trio`/`from_trio` fn-sig style
to the new unified `chan: LinkedTaskChannel` iface added in
prior commit (c46e9ee).

Deats,
- `to_trio.send_nowait(val)` (1st call) -> `chan.started_nowait(val)`
- `to_trio.send_nowait(val)` (subsequent) -> `chan.send_nowait(val)`
- `await from_trio.get()` -> `await chan.get()`

Converted fns,
- `sleep_and_err()`, `push_from_aio_task()` in
  `tests/test_infected_asyncio.py`
- `sync_and_err()` in `tests/test_root_infect_asyncio.py`
- `aio_streamer()` in
  `tests/test_child_manages_service_nursery.py`
- `aio_echo_server()` in
  `examples/infected_asyncio_echo_server.py`
- `bp_then_error()` in `examples/debugging/asyncio_bp.py`

Also,
- drop stale comments referencing old param names.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1303 to +1305
) -> AsyncIterator[
tuple[LinkedTaskChannel, Any]
]:
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open_channel_from() advertises AsyncIterator[tuple[LinkedTaskChannel, Any]], but the body yields (first, chan) where first is the started value and chan is the LinkedTaskChannel. Please fix the return type (and any related typing) to match the actual yielded tuple order, or swap the yield order to match the annotation.

Copilot uses AI. Check for mistakes.
Comment on lines 1337 to +1338
yield first, chan
# ^TODO! swap these!!
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an explicit # ^TODO! swap these!! next to yield first, chan, which indicates the tuple order is still in flux. Because this is a public-facing contextmanager API used throughout tests/examples, it should be finalized in this PR (choose an order and make typing/docs/callers consistent) to avoid follow-up breakage.

Suggested change
yield first, chan
# ^TODO! swap these!!
yield chan, first

Copilot uses AI. Check for mistakes.
Comment on lines 1114 to 1115
chan.started_nowait('Uhh we shouldve RTE-d ^^ ??')
await asyncio.sleep(float('inf'))
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise_before_started() raises at line 1112, so the subsequent chan.started_nowait(...) and await asyncio.sleep(...) are unreachable. If this is meant as explanatory text, it should stay commented/removed; otherwise restructure the test helper to exercise the intended path.

Suggested change
chan.started_nowait('Uhh we shouldve RTE-d ^^ ??')
await asyncio.sleep(float('inf'))
# The following lines are intentionally left as explanatory and
# are unreachable because of the unconditional `raise` above.
# chan.started_nowait('Uhh we shouldve RTE-d ^^ ??')
# await asyncio.sleep(float('inf'))

Copilot uses AI. Check for mistakes.
'''
Receive a value `asyncio.Task` <- `trio.Task`.

This is equiv to `await self._from_trio.get()`.
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LinkedTaskChannel.get() docstring says it's equivalent to await self._from_trio.get(), but the implementation awaits self._to_aio.get(). Please update the docstring (or rename the underlying attribute) so it matches the actual direction/source of messages.

Suggested change
This is equiv to `await self._from_trio.get()`.
This is equiv to `await self._to_aio.get()`.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api asyncio ur non-favorite stdlib module enhancement New feature or request streaming supervision

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants