-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-46752: Introduce task groups in asyncio #31270
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
Conversation
My plan is roughly the following: - [x] Copy the files from EdgeDb without modifications (named following asyncio conventions) - [ ] Bang on the tests until they will run - [ ] Bang on the tests until they pass - [ ] Remove pre-3.11 compatibility code - [ ] Switch from MultiError to ExceptionGroup - [ ] Other cleanup - [ ] Add a public API to tasks.py to replace `__cancel_requested__`
Two remaining issues: - [ ] _test_taskgroup_21 doesn't work (it didn't in EdgeDb either) - [ ] the test framework complains about a change to the event loop policy
This required some changes to the tests since EdgeDb's MultiError has some ergonomic conveniences that ExceptionGroup doesn't: - A helper method to get the types of the exceptions - It puts the number and types of the exceptions in the message Also, in one case (test_taskgroup_14) an EG nested inside another EG was raised, whereas the original code just raised one EG. This remains to be investigated.
Add to todo list: give the tests meaningful names.
That's probably useful only for tests. We could move assertExceptionIsLike to a mixin in test.support.
Subclassing was an afterthought. We assumed it's not needed, now I think we're assuming it would be rare. If you do subclass here, make sure you define derive(). Otherwise split() will not create parts of your type.
We could, it might be too long then though. |
Regarding If we have an agreement for it, I can prepare a pull request on the weekend. |
So it would, but I am worried about breaking code that depends on the current behavior -- often developers have no idea how to debug such a failure, and it would give 3.11 a bad rep "randomly breaks async apps". But maybe it's not so bad? Would any tests break?
Give it a whir and we can see if it's viable. If not, we can do something less drastic to just expose the cancel-requested state (maybe that's just |
Done (i.e., added to the todo list :-).
Perhaps. We'll see how often this comes up. I find my current solution manageable, even though it isn't as concise as Yury's code.
So we have a real decision to make here: do we need
Yeah, we might have to truncate if there are too many. But I already know that I've been confused regularly when I did |
d4aa783
to
d233dd1
Compare
Mystery solved: I wrote a small test program and found that the full error message contains the nested
So the |
FWIW I am strongly leaning towards not having a custom UPDATE: Done. |
I looked more into handling of This is the core of test 20: async def crash_soon():
await asyncio.sleep(0.1)
1 / 0
async def nested():
try:
await asyncio.sleep(10)
finally:
raise KeyboardInterrupt
async def runner():
async with taskgroups.TaskGroup() as g:
g.create_task(crash_soon())
await nested() We expect the Test 21 is almost the same except the I did add new tests that parallel test 20 and 21 but use I think there are good enough reasons why asyncio has these special cases for |
I just chatted with some Trio folks and they had decent use cases for allowing this. I'm going to look into how easy it would be to add this functionality. @1st1 |
I made bpo46729 for this and a draft PR so we can see the impact on tracebacks in the tests. |
We stop when there are no unfinished tasks, and then no new tasks can be created. (TODO: more thorough testing of edge cases?)
Looked pretty easy, so I made this change. (However, as soon as cancel scopes came up they started saying asyncio was dumb so I gave up asking more about that.) |
I didn't say asyncio was dumb. I said the way cancellation works is. You noted this yourself here. My generalization was unjust and I apologize for that. |
To get back to the matter at hand: localized timeouts are a pattern I often see in the wild, and they're somewhat problematic without cancel scopes. Take this example: async with timeout(5):
... Such a construct is provided not only by AnyIO and trio, but asyncio_timeout as well. The problem arises when the task is cancelled as a whole and the timeout expires, both before the event loop has a chance to raise the cancellation exception in the task. How, then, do you know if you need to simply exit the |
@agronholm If you want to discuss cancel scopes please open a new bpo issue. |
🤖 New build scheduled with the buildbot fleet by @gvanrossum for commit 9712241 🤖 If you want to schedule another build, you need to add the ":hammer: test-with-buildbots" label again. |
Agree. I personally don't see any blocker. |
Okay, let's merge once buildbots are green, then we can improve tests and docs in later PRs. BTW, are you interested in having something like your async-timeout in the stdlib as well? This might satisfy the Trio folks' desire for cancel scopes. (In order to work properly it would have to use .uncancel().) |
Yes, sure. I think the current API is stable and consistent. The only known problem is a case when timeout occurs for a task that was cancelled on the previous loop iteration. upd the mentioned |
Andrew, I also think it's time for us to add a context manager for timeouts to asyncio. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gvanrossum I think this is ready. With this merged we'll need to update the docs in a few places to de-prioritize asyncio.gather()
and steer people towards TaskGroups.
Are you folks acquainted with https://pypi.org/project/quattro/, since there's talk of timeout context managers in the stdlib? |
It's also a little unfortunate Quattro task groups weren't used instead, they are identical but with type annotations (and I think I fixed a minor issue with something, pickling?) |
A simple "thank you" would have sufficed. |
I've taken a quick look, the file seems identical with what Guido committed (except typing). If there's something you fixed and you can remember what it was, please file a bug or create a PR. Nit: I've also noticed that you removed the original copyright comment from the file. Not that I care in this specific case, but you shouldn't remove copyright notices from files you borrow from other projects unless you're explicitly permitted to do so. (Thanks for crediting in README though!) |
Type annotations are still not a thing in the standard library. Were you hoping for the code to be type annotated? @Tinche On another note, I am currently writing a BPO for potential low level asyncio cancellation machinery updates to support the implementation of cancel scopes and level cancellation in asyncio. The writing process has largely turned into a rubber ducking session and led to new insights on my part. The interesting aspect of Quattro is that it implements cancel scopes along with task groups, which this PR does not. It would be interesting to compare our implementations privately. I'll reach out to you by Gitter so as not to unnecessarily flood this PR. |
Sorry, I restored it just now. To me it's just clutter since the repo already has a license included. I see the TaskGroup here doesn't have it, so I might switch to that then ;) |
You should include the full copyright comment. |
While this change was merged, it is still being debated, and we may revise this. See e.g. bpo-46771. (In general, what was merged is intended as a starting point for further experiments and discussion, at least until feature freeze / beta 1, late May.) |
This is EdgeDB's TaskGroup class, adapted for Python 3.11.
In the individual commits you can see how I evolved this from the version in EdgeDB.
Here's a to-do list:
test_taskgroup_14
I get a nestedExceptionGroup
, while the original got a flat MultiError.TaskGroupError
? Implementing it correctly is awkward. We could just raiseBaseExceptionGroup
.If we decide to keep it, we need to fix it to do the hack where it inherits fromBaseException
iff at least one of the exceptions does. We also need to overridederive()
.BaseException
,.KeyboardInterrupt
andSystemExit
Maketest_taskgroup_21
work? (It wasn't working in EdgeDB either, so it was crudely disabled.)tasks.py
to replace__cancel_requested__
, and get rid of the monkey-patching.__aexit__()
is already waiting?.uncancel()
call in__aexit__()
(currently on line 97). Dropping that line does not cause any tests to fail.I also learned a few things about the ergonomics of
ExceptionGroup
:BaseException
hacks are clever but awkward to replicate in a subclass.str()
of an EG is rather sparse (Yury's version adds the number of sub-exceptions and their types).CC: @iritkatriel, @1st1
Co-authored-by: Yury Selivanov [email protected]
Co-authored-by: Andrew Svetlov [email protected]
https://bugs.python.org/issue46752