Repository navigation
In some cases asyncio.wait_for can lead to socket leak. #81839
Copy link
Copy link
Closed
Labels
Description
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
assignee = None
closed_at = None
created_at = <Date 2019-07-23.12:16:17.363>
labels = ['expert-asyncio', '3.7', 'performance']
title = 'In some cases asyncio.wait_for can lead to socket leak.'
updated_at = <Date 2022-03-11.17:35:41.865>
user = 'https://bugs.python.org/NikitaIlyasov'bugs.python.org fields:
activity = <Date 2022-03-11.17:35:41.865>
actor = 'dreamsorcerer'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['asyncio']
creation = <Date 2019-07-23.12:16:17.363>
creator = 'Nikita Ilyasov'
dependencies = []
files = ['48500', '50061']
hgrepos = []
issue_num = 37658
keywords = ['patch']
message_count = 18.0
messages = ['348328', '348329', '375937', '375942', '375947', '379542', '393566', '393567', '393584', '393586', '393687', '394249', '404998', '407248', '407252', '407373', '414797', '414925']
nosy_count = 12.0
nosy_names = ['ods', 'asvetlov', 'chris.jerdonek', 'Elvis.Pranskevichus', 'lukasz.langa', 'yselivanov', 'miss-islington', 'Nikita Ilyasov', 'dreamsorcerer', 'aaliddell', 'nmatravolgyi', 'kumaraditya']
pr_nums = ['21894', '21964', '21965', '21969', '26097', '29202', '29832', '29831']
priority = 'normal'
resolution = None
stage = 'patch review'
status = 'open'
superseder = None
type = 'resource usage'
url = 'https://bugs.python.org/issue37658'
versions = ['Python 3.6', 'Python 3.7']Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
NikitaIlyasov commentedon Jul 23, 2019
In some cases
asyncio.wait_forcan lead to socket leak.Condensed example:
If my understanding is correct
wait_forshould work in exactly 2 waysI provided source code for client and server so the problem can be easily reproduced on your system.
certificate and key can be easily generated with
minicaI found out that if I catch
CancelledErrorand add adone_callbackto the inner task, like so:then inside of
_done_callbackI can access the transport and protocol object and close the transport manually to prevent leaking.I run
netstat -a | grep 5000 | grep ESTAB | awk '{ print $5 }' | sort | uniq -c | grep 5000after the script is done and there are many unclosed connections.The output depends on your hardware so you might need to tweak the timeout parameter
NikitaIlyasov commentedon Jul 23, 2019
server example: https://gist.github.com/hexrain/bc92aa70eebc229365f0ce4bcccf7fc4
1st1 commentedon Aug 26, 2020
New changeset a2118a1 by Elvis Pranskevichus in branch 'master':
bpo-37658: Fix asyncio.wait_for() to respect waited task status (bpo-21894)
a2118a1
ambv commentedon Aug 26, 2020
New changeset 9de6be4 by Miss Islington (bot) in branch '3.9':
bpo-37658: Fix asyncio.wait_for() to respect waited task status (GH-21894) (GH-21964)
9de6be4
1st1 commentedon Aug 26, 2020
New changeset 6e1954c by Miss Islington (bot) in branch '3.8':
bpo-37658: Fix asyncio.wait_for() to respect waited task status (GH-21894) (bpo-21965)
6e1954c
cjerdonek commentedon Oct 24, 2020
Issue bpo-42130 that was recently filed appears related to this change.
ods commentedon May 13, 2021
The current solutions doesn't look correct. It swallows cancelling causing task to hang: https://bugs.python.org/issue42130 .
Proposed test case calls cancel for inner future and set_result for outer task in the same loop step. The old (prior to this patch) behaviour in this case looks correct to me. We have to elaborate on original problem to find the source of actual race.
Therefore I've tried to reproduce the problem with original code sample and published the adapted version of it here: https://github.com/ods/bpo-37658 . Nikita Ilyasov, please correct me if I've misunderstood what you meant. My result is that proposed solution actually doesn't solve the problem, but rather lowers the chance to get it. Namely, I get the traceback quoted in README both with 3.8.5, 3.9.4, and current code in main.
aaliddell commentedon May 13, 2021
Some discussion leading up to that change is here MagicStack/asyncpg#548 and in the issues it links.
ods commentedon May 13, 2021
The original problem can be fixed by wrapping await into try-except block:
I've updated my example to reproduce this: ods/bpo-37658@eca3d81
I believe it's general problem with maintaining atomicity in async code, and not a bug in
wait_for. Probably having an interface likeasync with loop.create_connection(…) as transport, protocolmight simplify correct usage for this particular case.aaliddell commentedon May 13, 2021
Wrapping every resource allocating call like that is what we were trying to avoid, since it makes wait_for go from a simple one-line helper to something you have to be very careful with.
Conceptually, a user should expect that wait_for should behave the exact same as awaiting the underlying awaitable, just with auto-cancellation. The problem with the current wait_for is that there is a gap where the underlying task may have completed but a cancellation arrives. In this case, we need to raise the cancellation to be a good asyncio citizen, but the underlying task has no opportunity to act on the cancellation (to free the resource) since it is already complete and cannot be re-entered. So the resource returned by the completed task gets stuck in limbo, since we can't return it and we can't assume a generic 'close' behaviour.
See my comment in the PR for a suggestion about an alternative structure for wait_for, which may avoid this gap and hence prevent the leak (but I have not tested it!)
nmatravolgyi commentedon May 14, 2021
I did a little research on how the
wait_forcould be improved. There are four alternate implementations you can find here: https://github.com/Traktormaster/wait-for2/blob/issue37658/wait_for2/impl.pyThe repository (on the linked branch) has tox set-up and a test that asserts the behaviour of builtin and alternate implementations.
Quick summary:
About the actually working alternate implementation I made:
In my opinion there is no way to implicitly handle losing a result properly in case of a cancellation, since it arbitrarily breaks the flow of execution. I'd look into having
wait_for(...)support cleanup callbacks when a cancellation and completion happens at the same time. Something like:The inner task is still responsible for handling the resource before it returns, which means if the inner task is cancelled, there must be no leak if the implementation is correct. If no cancellation happens, everything is "fine". Finally, the waiter task would be able to handle its cancellation and the simultaneous completion of the inner task if the caller code provides a callback.
Unfortunately this requires the user of
wait_forto be aware of this race-condition. However it makes it possible to be handled properly when the waited future's result requires a cleanup.ods commentedon May 24, 2021
Re: msg393586
Unfortunately this didn't close the gap, so the leak persisted.
I did some research on the source of the error. There are circular references between instances of _SelectorSocketTransport and _SSLProtocolTransport (see attached image), therefore these objects are destroyed by garbage collector. The order might be arbitrary. When _SelectorSocketTransport is destroyed after _SSLProtocolTransport everything is ok, but with the opposite order we get the error printed. Here is description on how to reproduce this:
https://github.com/ods/bpo-37658#circular-references-when-using-ssltls
1 remaining item
asvetlov commentedon Nov 29, 2021
New changeset 934a826 by Sam Bull in branch 'main':
bpo-37658: Actually return result in race condition (GH-29202)
934a826
miss-islington commentedon Nov 29, 2021
New changeset 305236e by Miss Islington (bot) in branch '3.10':
bpo-37658: Actually return result in race condition (GH-29202)
305236e
asvetlov commentedon Nov 30, 2021
New changeset 99a9b34 by Miss Islington (bot) in branch '3.9':
bpo-37658: Actually return result in race condition (GH-29202) (GH-29832)
99a9b34
kumaraditya303 commentedon Mar 9, 2022
@asvetlov Anything left to do here or can this be closed ?
Dreamsorcerer commentedon Mar 11, 2022
There is still an issue here. I proposed a possible solution at: #28149 (comment)
You can also scroll up through the lengthy discussion to see how I reached that conclusion. I've not had time yet to actually try implementing something yet.
zhuofeng6 commentedon Sep 9, 2022
any question about this issue? if not, i think it is better to close it
gvanrossum commentedon Sep 9, 2022
Yes, see above: