Skip to content

Backport py3.11 asyncio's taskgroups. #8791

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open

Conversation

smurfix
Copy link
Contributor

@smurfix smurfix commented Jun 20, 2022

This backports 3.11's TaskGroup class to uasyncio.

I don't care much about the except * stuff, not in µPy context anyway, but sane error recovery is crucial and taskgroups make this job a whole lot easier, not to mention much less bug prone. (Writing from a lot of Trio and anyio experience here.)

Bottom line: I refuse to write async code without using taskgroups. So here you are.

TODO: Write a couple of tests.

Closes #8508.

@smurfix smurfix force-pushed the tg branch 2 times, most recently from 5a46a80 to b5723ea Compare June 20, 2022 20:08
@smurfix
Copy link
Contributor Author

smurfix commented Jun 20, 2022

Sigh. It's not my commit message that's failing the test!

@dpgeorge dpgeorge added the extmod Relates to extmod/ directory in source label Jun 21, 2022
@dpgeorge
Copy link
Member

Wow, thanks for this! It's very nice to see that it can be implemented in pure Python. I don't know anything about TaskGroup so will need to learn how it works.

@dpgeorge
Copy link
Member

It's not my commit message that's failing the test!

You're right. That CI check will need fixing so it allows "Revert" commit messages.

@dpgeorge
Copy link
Member

It's not my commit message that's failing the test!

You're right. That CI check will need fixing so it allows "Revert" commit messages.

Actually the CI check is working OK, it's only supposed to check the commit messages that are unique to the PR.

If you rebase (and force push) on latest master it will hopefully get the CI green.

@smurfix
Copy link
Contributor Author

smurfix commented Jun 21, 2022

Pushed. Writing tests, found a significant bug (related to cancellation of course). Investigating.

@smurfix smurfix force-pushed the tg branch 8 times, most recently from 1a0dffd to 7d20532 Compare June 22, 2022 19:29
@smurfix
Copy link
Contributor Author

smurfix commented Jun 22, 2022

Huh. I have no idea where those test failures are coming from.

@dlech
Copy link
Contributor

dlech commented Jun 22, 2022

The CI logs say error is:

+micropython-coverage: ../../py/emitnative.c:2806: emit_native_raise_varargs: Assertion `n_args == 1' failed.

It looks like the bytecode emitter allows 2 args but the native emitter only allows 1.

@smurfix
Copy link
Contributor Author

smurfix commented Jun 22, 2022

It looks like the bytecode emitter allows 2 args but the native emitter only allows 1.

Well, yeah, I saw that line, but what has that got to do with implementing __hash__ and/or how do I fix that? I didn't touch no emitters … so either I'm doing something really dumb here, or I found a bug which I'm not qualified to fix, esp. since the code works on my system(s).

@dlech
Copy link
Contributor

dlech commented Jun 23, 2022

Assuming you are using the unix port, to reproduce locally, try make -C ports/unix test_full or run micropython with -X emit=native.

There are a number of tests that are disabled when emit=native because of raise_varargs already, basics/try_reraise.py, basics/try_reraise2 and basics/try_finally_return2.py to name a few (search for raise_varargs in tests/run-tests.py).

I suspect you have written some Python code similar to one of these that causes the same error.

@peterhinch
Copy link
Contributor

I'm running this on a Pyboard 1.1 - TaskGroups are a very nice feature.

Something Google seems loath to divulge is how to access return values from the members of a group (assuming they terminate normally).

@smurfix
Copy link
Contributor Author

smurfix commented Jun 25, 2022

@peterhinch Glad that my code is of use. I'll try to fix up the CI errors shortly; battling with Covid aftereffects, so if somebody else wants to fix up this PR, feel free.

You need to return any values explicitly, e.g. by setting an object attribute to it or queuing the value or whatever.

@peterhinch
Copy link
Contributor

Sorry you're unwell - I hope you feel better soon.

Writing tests, found a significant bug (related to cancellation of course). Investigating.

Is this still outstanding or is the code ready for review?

I very much hope this is implemented: in many applications it's a big improvement over gather.

@smurfix
Copy link
Contributor Author

smurfix commented Jun 26, 2022

Is this still outstanding or is the code ready for review?

No, that's done. The workaround I implemented admittedly isn't particularly clean, but it gets the job done and doesn't alter non-taskgroup code (as that might introduce bugs or slowdowns).

@peterhinch
Copy link
Contributor

@dpgeorge @jimmo This is to advocate for TaskGroup with a real world example where they fit the bill perfectly.

Consider a communication link between two peers using an unreliable medium such as WiFi. This may be near the limit of range and the link can fail in a variety of ways. The simplest way to recover is the "belt and braces" approach: if a peer encounters an unrecoverable error, it takes the link down for a period long enough for the other peer also to suffer an unrecoverable error. That way both peers start from a "power up" state.

A TaskGroup might then comprise:

  • Task trying to keep the WiFi available through brief outages.
  • Task waiting on received messages.
  • Task pinging the other peer and checking a response.
  • Intermittently a task is added to the group. It sends a message, awaits an ack, and terminates.

If any of these experience an error that cannot be handled locally, the exception is passed up to be handled by the task that created the group. When this occurs, every task in the group terminates in an orderly way, running cleanup code in finally clauses or via context managers.

The TaskGroup is ideal for this - and in my testing works fine :) I rest my case...

@smurfix
Copy link
Contributor Author

smurfix commented Jun 27, 2022

@peterhinch Exactly. Unstructured tasks (like asyncio's baseline tasks) mean that you have to keep track of what's still running and what might have to be cancelled and/or restarted on your own. Code doing that tends to contain a heap of bugs you can't test for, and basically doesn't scale.

In contrast, when a taskgroup's async context manager has ended you know that there's no dangling tasks, unprocessed finally clauses, unreported exceptions, or similar nonsense around; you can proceed with confidence. When (not if) the framework ensures that whole classes of errors can't happen, you no longer have to test for them, or even think about them.

The latter point is much more powerful than you'd assume at first glance.

@smurfix
Copy link
Contributor Author

smurfix commented Oct 2, 2023

Yes I know this is one of >300 open PRs … nevertheless, I'd appreciate feedback WRT this patch's mergeability.

tannewt added a commit to tannewt/circuitpython that referenced this pull request Jan 23, 2024
Added support for Columbia DSL Sensor board
@ZanderBrown
Copy link

ZanderBrown commented Aug 24, 2024

Once again I try and use this, convinced that it was merged already, only to crash and burn 🙈

Having task groups in CPython may have ruined me…

@smurfix
Copy link
Contributor Author

smurfix commented Aug 27, 2024

Yeah. Sigh. I'll do a re-merge shortly.

Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
@smurfix
Copy link
Contributor Author

smurfix commented Aug 27, 2024

Ugh. Don't ask me what this CI error means …

Copy link
Contributor

@projectgus projectgus left a comment

Choose a reason for hiding this comment

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

Coming in late to this. I haven't used Taskgroups in CPython but I can see the utility - this looks really useful. Thanks for all the work and persistence.

Ugh. Don't ask me what this CI error means …

+Traceback (most recent call last):
+  File "<stdin>", line 13, in <module>
+AttributeError: 'module' object has no attribute 'ExceptionGroup'
+

FAILURE /home/runner/work/micropython/micropython/tests/results/extmod_asyncio_taskgroup.py

webassembly port has its own version of the asyncio module under ports/webassembly/asyncio. I think you've got two options here:

  1. Add extmod/asyncio/taskgroup.py to ports/webassembly/variants/manifest.py and ports/webassembly/asyncio/__init__.py
  2. Or update the test to skip if the asyncio module doesn't contain this name

@@ -170,6 +171,47 @@ async def start_server(cb, host, port, backlog=5):
return srv


# Helper task to run a TCP stream server.
# Callbacks (i.e. connection handlers) may run in a different taskgroup.
async def run_server(cb, host, port, backlog=5, taskgroup=None):
Copy link
Contributor

@projectgus projectgus Aug 28, 2024

Choose a reason for hiding this comment

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

Did you end up bringing this up with CPython, @smurfix? It's probably the biggest remaining hurdle - to try and keep asyncio module contents close to CPython.

I guess the alternative is to hide this in a different module somewhere (maybe in micropython-lib). However if CPython were up for solving this as well, that would be ideal.

# Try not to allocate a SleepHandler on the heap if possible
def sleep_ms(t, sgen=SleepHandler()):
if sgen.state is not None: # the static one is busy
sgen = SleepHandler()
Copy link
Member

Choose a reason for hiding this comment

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

This definitely needs to be understood better. It should be that the scheduler always iterates the singleton immediately. If that's no longer the case then we need to understand why and document it.

Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
@smurfix smurfix force-pushed the tg branch 3 times, most recently from 00d4d17 to 2be6e78 Compare August 28, 2024 11:28
Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
`run_server` is not in CPython. We'd like to avoid that.

Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
Signed-off-by: Matthias Urlichs <matthias@urlichs.de>
@smurfix
Copy link
Contributor Author

smurfix commented Apr 11, 2025

Updated to current master.

The CPython discussion on what to do about the Taskgroup.cancel method has stalled for now (and mainly centered on the name of the feature, as opposed to how it's supposed to work). That doesn't surprise me because the semantics of cancellation are somewhat murky in asyncio, but as cancelling all tasks in a taskgroup is something reasonable to do …

+mpy-cross: ../py/emitnative.c:3010: emit_native_raise_varargs: Assertion n_args == 1' failed.`

Umm, that used not to happen AFAIR?

It should be that the scheduler always iterates the singleton immediately

Well, it appears not to. In any case, by its nature async code is async: nothing prevents me from calling a bare asyncio.sleep and then doing something else before using the result in an await. Thus we shouldn't depend on that whether or not it's supposed to apply in the standard case.

    async def not_too_fast(delay, proc):
        dly = asyncio.sleep(delay)
        res = await proc()
        await dly
        return res

In MicroPython this is a nicely concise way of saying "give me the result of proc in exactly two seconds, except when proc takes longer than that`. (This doesn't work the same way on CPython, but not by design AFAICT.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
extmod Relates to extmod/ directory in source
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PEP 654: Exception groups and except *, leading to asyncio TaskGroup
10 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy