-
-
Notifications
You must be signed in to change notification settings - Fork 30.7k
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
bpo-36607: Eliminate RuntimeError raised by asyncio.all_tasks() #13971
Conversation
If internal tasks weak set is changed by another thread during iteration.
Lib/asyncio/tasks.py
Outdated
try: | ||
tasks = list(_all_tasks) | ||
except RuntimeError: | ||
pass |
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.
I don't like unchecked loops like that. There must be a retry count or something.
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.
The number of iterations is the question.
What is good enough to prevent a collision? 10
? 100
? 1000
?
I think the number depends on count of threads and a number of tasks inside every processed loop.
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.
I'd say 1000 should be enough. It's high enough to give it enough attempts and it's low enough to abort early if something weird is going on.
When you're done making the requested changes, leave the comment: |
Lib/asyncio/tasks.py
Outdated
@@ -42,9 +42,19 @@ def all_tasks(loop=None): | |||
"""Return a set of all tasks for the loop.""" | |||
if loop is None: | |||
loop = events.get_running_loop() | |||
# NB: set(_all_tasks) is required to protect | |||
# NB: list(_all_tasks) is required to protect | |||
# from https://bugs.python.org/issue34970 bug |
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.
Can we write a proper comment here without referencing an issue on bpo? I mean a url is fine, but it requires the reader to navigate to it in order to understand what's going on.
Lib/asyncio/tasks.py
Outdated
# from https://bugs.python.org/issue34970 bug | ||
return {t for t in list(_all_tasks) | ||
# NB: Have to repeat on RuntimeError, other thread |
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.
There's no point in prefixing all comments with "NB"; it's just a regular comment. I'd suggest to write something along the following (as it took me sometime to understand why this change is necessary at all):
"Looping over a WeakSet (_all_tasks) isn't safe as it can be updated from another thread while we do so. Therefore we cast it to list prior to filtering. The list cast itself requires iteration, so we repeat it several times ignoring RuntimeErrors (which are not very likely to occur). See issues 34970 and 36607 for details."
Thanks @asvetlov for the PR 🌮🎉.. I'm working now to backport this PR to: 3.7, 3.8. |
…onGH-13971) If internal tasks weak set is changed by another thread during iteration. https://bugs.python.org/issue36607 (cherry picked from commit 65aa64f) Co-authored-by: Andrew Svetlov <[email protected]>
GH-13975 is a backport of this pull request to the 3.8 branch. |
…onGH-13971) If internal tasks weak set is changed by another thread during iteration. https://bugs.python.org/issue36607 (cherry picked from commit 65aa64f) Co-authored-by: Andrew Svetlov <[email protected]>
GH-13976 is a backport of this pull request to the 3.7 branch. |
This approach feels very wrong to me as we aren't fixing the underlying problem of multiple threads mutating the same global variable without locks. Some more concrete issues:
In my opinion the only valid solution to this problem is to remove the possibility of two threads mutating the |
…3971) If internal tasks weak set is changed by another thread during iteration. https://bugs.python.org/issue36607 (cherry picked from commit 65aa64f) Co-authored-by: Andrew Svetlov <[email protected]>
Using a lock is problematic because of GC. Making all_tasks a loop property is a good idea. Andrew? |
Yeah locks like be either impossible or almost impossible to implement correctly to solve this. I mentioned this on the issue as a complexity of trying to solve this using locks.
|
One downside to make this a loop attribute is that the attribute becomes part of the AbstractEventLoop api. Other implementations (uvloop, tokio) will need to implement it. Another option is to add an indirection: another mapping of loop -> all_tasks |
Ideally yes.
To protect the current try-again approach: yes, theoretically it still can fail, the PR just reduces a chance of failure by several orders of magnitude. Suppose we have a code:
Local If |
Re adding new API: I think I addressed that in my last comment :) |
Yes. Sorry, I read your comment just after sending mine |
I agree that we shouldn't try and support this because using loops across threads is not supported already.
Correct me if I am wrong as I am not very familiar with the C implementations but I feel like this type of invalid data is very different from the time of copy vs time of use problem you are describing:
|
For reference we solved this in our backported version with this hack: https://gist.github.com/nickdavies/4a37c6cd9dcc7041fddd2d2a81cee383 the key difference is the |
We do support it.
Two copies are impossible IMHO. Anyway, we make a set again before returning from Valid tasks miss is possible if CPython switches threads in approximately every 5 ms (see sys.getswitchinterval()). So, another implementation can save second (rare enough) |
I just don't understand how having code in the core CPython implementation that is correct most of the time is at all acceptable. It doesn't matter how unlikely it is, I expect the language itself to be correct 100% of the time. We should really aim for a deterministic fix. |
I think we should revert the change and implement it via the loop->all_tasks mapping. Or just revert it and don't fix if you have no time. |
@1st1 and I agree that the PR is not perfect. https://bugs.python.org/issue36607 is reverted to "open" state. Please feel to propose the PR which will be awesome! |
Yeah, I'm OK with this fix for 3.7. The main reason is that this allows us to not touch a bunch of low-level C code in a bugfix release. For 3.8 we'll do better. |
…3971) If internal tasks weak set is changed by another thread during iteration. https://bugs.python.org/issue36607 (cherry picked from commit 65aa64f) Co-authored-by: Andrew Svetlov <[email protected]>
@nickdavies your patch still has a low chance of failure. A probability of failure is very-very low but still not zero |
…onGH-13971) If internal tasks weak set is changed by another thread during iteration. https://bugs.python.org/issue36607
If internal tasks weak set is changed by another thread during iteration.
https://bugs.python.org/issue36607