Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
gvanrossum merged 24 commits intopython:mainfromgvanrossum:taskgroups
Feb 15, 2022

Conversation

gvanrossum
Copy link
Member

@gvanrossumgvanrossum commentedFeb 11, 2022
edited by 1st1
Loading

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:

  • Figure out why intest_taskgroup_14 I get a nestedExceptionGroup, while the original got a flat MultiError.
  • Fix the test framework's complaint about the event loop policy being changed.
  • Design question: Do we needTaskGroupError? 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().
  • Add tests for handlingBaseException,KeyboardInterrupt andSystemExit.
  • Maketest_taskgroup_21 work? (It wasn't working in EdgeDB either, so it was crudely disabled.)
  • Add a public API totasks.py to replace__cancel_requested__, and get rid of the monkey-patching.
  • Design question: Should we allow creating new tasks while__aexit__() is already waiting?
  • Add a test showing the need for the.uncancel() call in__aexit__() (currently on line 97). Dropping that line does not cause any tests to fail.
  • Ensure the taskgroup tests are run with the C and Python Task implementations.
  • Rename tests to have meaningful names.
  • I have a few ideas for minor cleanups that I will do later.
  • What should we do with the copyright header?@1st1.
  • Create a bpo issue.
  • Add a NEWS blurb.
  • Documentation and What's New entry (in a separate PR, probably).

I also learned a few things about the ergonomics ofExceptionGroup:

  • TheBaseException hacks are clever but awkward to replicate in a subclass.
  • Thestr() of an EG is rather sparse (Yury's version adds the number of sub-exceptions and their types).
  • Yury's version also has a convenience method that returns the set of sub-exception types.
  • Unsure about the nesting issue (see above).

CC:@iritkatriel,@1st1

Co-authored-by: Yury Selivanovyury@edgedb.com
Co-authored-by: Andrew Svetlovandrew.svetlov@gmail.com

https://bugs.python.org/issue46752

kumaraditya303, FrankCasanova, matusvalo, JonasKs, Yalye, and ikheifets reacted with thumbs up emojicj81499 reacted with hooray emojiwwarne, achimnol, ikheifets, scastlara, and nashaad reacted with heart emoji
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 MultiErrorhas 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 messageAlso, in one case (test_taskgroup_14) an EG nested inside anotherEG was raised, whereas the original code just raised one EG.This remains to be investigated.
@iritkatriel
Copy link
Member

iritkatriel commentedFeb 11, 2022
edited
Loading

Add to todo list: give the tests meaningful names.

Yury's version also has a convenience method that returns the set of sub-exception types.

That's probably useful only for tests. We could moveassertExceptionIsLike to a mixin in test.support.

The BaseException hacks are clever but awkward to replicate in a subclass.

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 definederive(). Otherwise split() will not create parts of your type.

The str() of an EG is rather sparse (Yury's version adds the number of sub-exceptions and their types).

We could, it might be too long then though.

@asvetlov
Copy link
Contributor

Regarding__cancel_requested__, I support the change and have a follow-up proposal: returnFalse fromcancel() call if__cancel_requested__ was setand don't override the cancellationmsg in this case. I believe it makesTask.cancel() design in line withFuture.cancel().

If we have an agreement for it, I can prepare a pull request on the weekend.

@gvanrossum
Copy link
MemberAuthor

Regarding__cancel_requested__, I support the change and have a follow-up proposal: returnFalse fromcancel() call if__cancel_requested__ was setand don't override the cancellationmsg in this case. I believe it makesTask.cancel() design in line withFuture.cancel().

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?

If we have an agreement for it, I can prepare a pull request on the weekend.

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 justt.cancelled()) and add an API to reset it.

@gvanrossum
Copy link
MemberAuthor

gvanrossum commentedFeb 11, 2022
edited
Loading

Add to todo list: give the tests meaningful names.

Done (i.e., added to the todo list :-).

Yury's version also has a convenience method that returns the set of sub-exception types.

That's probably useful only for tests. We could moveassertExceptionIsLike to a mixin in test.support.

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.

The BaseException hacks are clever but awkward to replicate in a subclass.

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 definederive(). Otherwise split() will not create parts of your type.

So we have a real decision to make here: do we needTaskGroupException or can we just raiseExceptionGroup?

The str() of an EG is rather sparse (Yury's version adds the number of sub-exceptions and their types).

We could, it might be too long then though.

Yeah, we might have to truncate if there are too many. But I already know that I've been confused regularly when I didprint(exc) whereexc was an EG, and it printedjust the message. (repr(exc) is much better.) Maybe just put the count in there? Presumably this would be a change in the PEP based on practical experience during the alpha/beta period.

@gvanrossum
Copy link
MemberAuthor

Figure out why intest_taskgroup_14 I get a nestedExceptionGroup, while the original got a flatMultiError.

Mystery solved:test_taskgroup_14 doesnot get a flatMultiError! It's just thatassertRaisesRegex(E, pat) looks forpat anywhere in the exception's error message, andMultiError includes the complete traceback for all suberrorsin the message! (Because it has to work with Python versions that don't print nested tracebacks.)

I wrote a small test program and found that the full error message contains the nestedMultiError:

edb.common.taskgroup.TaskGroupError: unhandled errors in a TaskGroup; 1 sub errors: (TaskGroupError) + TaskGroupError: unhandled errors in a TaskGroup; 1 sub errors: (ZeroDivisionError) + ZeroDivisionError: division by zero |   File "t.py", line 7, in crash_after |     1/0 |   File "t.py", line 13, in amain |     h.create_task(crash_after(0.5)) |   File "/Users/guido/edgedb/edb/common/taskgroup.py", line 170, in __aexit__ |     raise me from None

So theassertRaisesRegex() call matched on the second line of that. (@1st1: It's not obvious from the way the test was written that this was intentional.)

@gvanrossum
Copy link
MemberAuthor

gvanrossum commentedFeb 11, 2022
edited
Loading

FWIW I am strongly leaning towards not having a customTaskGroupError. Theasyncio package only defines a few custom exceptions, and those seem semantically important (e.g. they convey that a very specific error occurred). It usually raises builtin exceptions likeTypeError,ValueError,RuntimeError orConnectionResetError.

UPDATE: Done.

@gvanrossum
Copy link
MemberAuthor

I looked more into handling ofBaseException.

This is the core of test 20:

asyncdefcrash_soon():awaitasyncio.sleep(0.1)1/0asyncdefnested():try:awaitasyncio.sleep(10)finally:raiseKeyboardInterruptasyncdefrunner():asyncwithtaskgroups.TaskGroup()asg:g.create_task(crash_soon())awaitnested()

We expect therunner() to terminate withKeyboardInterrupt. This works, since theKeyboardInterrupt transfers control directly into__aexit__, which squirrels it away and eventually raises it (without using an exception group).

Test 21 is almost the same except thecrash_soon() coroutine raisesKeyboardInterrupt andnested() raises a regular exception. We would still likerunner() to exit withKeyboardInterrupt, but in this case theKeyboardInterrupt exception has to travel through asyncio, which (intentionally) treatsKeyboardInterrupt andSystemExit special so that they immediately terminate the event loop. Therefore, test 21 doesn't work. (To make it work, asyncio would have to stop makingKeyboardInterrupt andSystemExit special cases.)

I did add new tests that parallel test 20 and 21 but useMyBaseExc instead ofKeyboardInterrupt, and those raise, as expected, aBaseExceptionGroup wrapping both theMyBaseExc and the other exception.

I think there are good enough reasons why asyncio has these special cases forKeyboardInterrupt andSystemExit, so for now I'll leave test 21 disabled.

@gvanrossum
Copy link
MemberAuthor

Design question: Should we allow creating new tasks while__aexit__() is already waiting?

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

@iritkatriel
Copy link
Member

iritkatriel commentedFeb 12, 2022
edited by gvanrossum
Loading

Yeah, we might have to truncate if there are too many. But I already know that I've been confused regularly when I didprint(exc) whereexc was an EG, and it printedjust the message. (repr(exc) is much better.) Maybe just put the count in there? Presumably this would be a change in the PEP based on practical experience during the alpha/beta period.

I madebpo46729 for this and adraft 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?)
@gvanrossum
Copy link
MemberAuthor

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

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.)

@agronholm
Copy link
Contributor

I didn't say asyncio was dumb. I said the way cancellation works is. You noted this yourselfhere. My generalization was unjust and I apologize for that.

@agronholm
Copy link
Contributor

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:

asyncwithtimeout(5):   ...

Such a construct is provided not only by AnyIO and trio, butasyncio_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 thetimeout() context manager or the entire task?

blakev reacted with thumbs up emoji

@gvanrossum
Copy link
MemberAuthor

@agronholm If you want to discuss cancel scopes please open a new bpo issue.

@bedevere-bot
Copy link

🤖 New build scheduled with the buildbot fleet by@gvanrossum for commit9712241 🤖

If you want to schedule another build, you need to add the ":hammer: test-with-buildbots" label again.

@bedevere-botbedevere-bot removed the 🔨 test-with-buildbotsTest PR w/ buildbots; report in status section labelFeb 15, 2022
@asvetlov
Copy link
Contributor

asvetlov commentedFeb 15, 2022
edited
Loading

Agree. I personally don't see any blocker.
But I want to improve tests a little, sure.

@gvanrossum
Copy link
MemberAuthor

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().)

@asvetlov
Copy link
Contributor

asvetlov commentedFeb 15, 2022
edited
Loading

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.
Now, this bug can be fixed easily because of thetask.cancelling() method and changed.cancel() behavior. I can prepare a pull request fortimeout async context manager inclusion into asyncio.

upd the mentionedasync-timeout bug is actually auto-fixed after this PR merging, no timeout source code change is required. The same probably is true forasyncio.wait_for().

gvanrossum reacted with thumbs up emoji

@1st1
Copy link
Member

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.

Andrew, I also think it's time for us to add a context manager for timeouts to asyncio.

asvetlov, tomchristie, Masynchin, and rednafi reacted with thumbs up emoji

Copy link
Member

@1st11st1 left a comment
edited
Loading

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-prioritizeasyncio.gather() and steer people towards TaskGroups.

tomchristie reacted with thumbs up emoji
@gvanrossumgvanrossum merged commit602630a intopython:mainFeb 15, 2022
@Tinche
Copy link
Contributor

Are you folks acquainted withhttps://pypi.org/project/quattro/, since there's talk of timeout context managers in the stdlib?

@Tinche
Copy link
Contributor

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?)

@gvanrossum
Copy link
MemberAuthor

A simple "thank you" would have sufficed.

@gvanrossumgvanrossum deleted the taskgroups branchFebruary 16, 2022 04:54
@1st1
Copy link
Member

@Tinche

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?)

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!)

@agronholm
Copy link
Contributor

It's also a little unfortunate Quattro task groups weren't used instead, they are identical but with type annotations

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.

@Tinche
Copy link
Contributor

Nit: I've also noticed that you removed the original copyright comment from the file.

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 ;)

@gvanrossum
Copy link
MemberAuthor

gvanrossum commentedFeb 16, 2022
edited by bedevere-bot
Loading

@Tinche I've created a separate issue for cancel scopes:bpo-46771.

@1st1
Copy link
Member

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.

@gvanrossum
Copy link
MemberAuthor

gvanrossum commentedFeb 17, 2022
edited
Loading

HOWEVER, this introduces a change in behavior around task cancellation:

  • A task that catchesCancelledError is allowed to run undisturbed (ignoring further.cancel() callsand allowing any number ofawait calls!) until it either exits or calls.uncancel().

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.)

Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@asvetlovasvetlovasvetlov approved these changes

@1st11st11st1 approved these changes

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

8 participants
@gvanrossum@iritkatriel@asvetlov@agronholm@1st1@bedevere-bot@Tinche@the-knights-who-say-ni

[8]ページ先頭

©2009-2025 Movatter.jp