You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
asyncio.StreamReader.read hangs for reused socket file descriptors when asyncio.StreamWriter.close() is not called #88968
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=Noneclosed_at=Nonecreated_at=<Date2021-08-02.07:45:23.250>labels= ['type-bug', '3.9', 'expert-asyncio']
title='asyncio.StreamReader.read hangs for reused socket file descriptors when asyncio.StreamWriter.close() is not called'updated_at=<Date2021-10-24.08:09:32.313>user='https://github.com/ntc2'
with a already connected socket socket, if you call socket.close()
but not w.close() when you're done, then when the OS later reuses
the file descriptor of socket for a new socket, and that new socket
is used with
(r,w) = asyncio.open_connection(sock=socket)
again, the r.read(...) for the new r can hang indefinitely, even
when data is available on the underlying socket. When the hang
happens, closing the socket on the writer side doesn't help, and the
socket gets stuck forever in the CLOSE_WAIT state on the reader
side. Using strace shows that the reader side is stuck in epoll_wait(...).
Client and server programs that reproduce the bug
=================================================
Run the server in one shell and then run the client in the other
shell. They each take one argument, that controls how they close their
sockets/streams when they're done.
Behavior for different combinations of closure modes
====================================================
Perhaps this is overkill, but here's what happens for all 15 possible
combinations of how the client and server close their connections.
For example, "Client=S, Server=S" below means we run $ python3 client.py S and $ python3 server.py S. Sometimes multiple
combinations have the same behavior, so they're grouped together
below.
Client hangs on r.read on second connection, and killing the server
on the other end has no effect, with the socket stuck in CLOSE_WAIT on
the client side forever.
Everything works! But here we see that the client and/or server
(whichever side is using '' for mode) is not reusing the socket file
descriptors right away (have to wait for GC). This is evidence that
the problem is due to stale state in asyncio tied to the reused file
descriptors.
https://bugs.python.org/issue35065: reading from a closed stream can
hang. Could be related if the problem is due to aliased streams from
reused file descriptors.
Oh, and I can't count: there are 16 = 4x4 possible combinations of socket closure modes for the client and server. The one I missed was Client='', Server=SA, where everything works because the client doesn't reuse file descriptors and the server closes its sockets correctly.
ntc2 commented on Oct 24, 2021
ntc2mannequin
MannequinAuthor
More actions
Just wanted to clarify: my previous "where everything works" comment is not saying this bug doesn't exist, I just mean I missed one case in my analysis of the bug. The bug is very much there, and easy to reproduce using the example programs I attached.
Since the OP has invested much effort in the analysis, maybe they can submit a PR using a fix? We're still short of asyncio experts and it's a lot easier to review a PR than it is to come up with a fix for a complex bug like this.
Also, the first step should be if this is still a problem with 3.11 (or the main branch).
Yes, this is still an issue, I just tested against 6cbbc26. For example, the python3 server.py A and python3 client.py S combination fails as originally described.
Sorry that the issue description is so large, I may have gotten carried away describing the test cases. But the high level description of the issue is not complex. I'll repeat it again here, trying to be more clear. Let me know if it still doesn't make sense.
Problem
The asyncio streams API provides open_connection, which lets you create a reader-writer stream pair (r,w) from an already connected socketsocket, like this:
(r,w) = asyncio.open_connection(sock=socket)
There are two different close functions you can call when you're done with the stream -- w.close() and socket.close() -- and your program can hang if you use socket.close() but not w.close(). In particular, the hang happens when the OS reuses the underlying file descriptor of socket in a new socket socket2, you create a new stream with (r2,w2) = asyncio.open_connection(sock=socket2), and you then call r2.read(...). When the hang happens, the socket socket2 is stuck in the CLOSE_WAIT state.
Example Programs Illustrating the Bug
There are simple example programs attached to the original bug report. You can reproduce the problem by running python3 server.py A in one shell followed by python3 client.py S in another shell.
I don't know anything about how asyncio is implemented internally, so I have no idea how to fix this issue, but hopefully the simple example programs and ease of reproducing the bug will make it obvious to someone familiar with the asyncio internals. If no one who understands the asyncio implementation is available I can take a stab at it myself.
It's perhaps possible to call self._ensure_fd_no_transport(sock) in create_connection(), except the latter lives in BaseEventLoop, while _ensure_fd_no_transport() is defined in a subclass thereof, BaseSelectorEventLoop. We can probably make this work by overriding create_connection() in the latter. Or maybe the check can be moved to _create_connection_transport().
I looked at your doc changes in #97936. In conjunction with a defensive "fail fast" fix, like making sure that _ensure_fd_no_transport(sock) is always called as part of asyncio.open_connection(sock=socket), the doc changes seem sufficient. But if for some reason it's not possible to ensure that _ensure_fd_no_transport is always called, and so the possibility of the more subtle failure mode remains (hanging later when the socket fd is reused), then I think the warning in the docs should be more harsh and explicitly tell the user that calling socket.close() after the ownership transfer may lead to subtle bugs in their program.
Anyway, thanks for debugging this! Making sure that _ensure_fd_no_transport is always called as part of open_connection seems like a great solution from the user friendliness point of view, and would have saved me hours of debugging when I ran into this problem.
Activity
ntc2 commentedon Aug 2, 2021
Problem
=======
When using asyncio streams via
with a already connected socket
socket, if you callsocket.close()but not
w.close()when you're done, then when the OS later reusesthe file descriptor of
socketfor a new socket, and that new socketis used with
again, the
r.read(...)for the newrcan hang indefinitely, evenwhen data is available on the underlying socket. When the hang
happens, closing the socket on the writer side doesn't help, and the
socket gets stuck forever in the CLOSE_WAIT state on the reader
side. Using
straceshows that the reader side is stuck inepoll_wait(...).Client and server programs that reproduce the bug
=================================================
Run the server in one shell and then run the client in the other
shell. They each take one argument, that controls how they close their
sockets/streams when they're done.
Usage: python3 client.py CLOSE_MODE
Usage: python3 server.py CLOSE_MODE
Where CLOSE_MODE can be
open_connectionsocket usingsocket.socket.close()open_connectionsocket usingasyncio.StreamWriter.close()open_connectionsocket both waysThese are also attached, but here's the source.
The
client.py:The
server.py:Example session:
server.py Sandclient.py A================================================
Note that file descriptor 7 is reused on the server side, before the
server hangs on
r.read.Run the server in one shell:
$ python3 server.py S Listening on 127.0.0.2:12345 <socket.socket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('0.0.0.0', 0)> Client connected from 127.0.0.3:34135 <socket.socket fd=7, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.2', 12345), raddr=('127.0.0.3', 34135)> -> b'server' <- b'client' Closed client socket Client connected from 127.0.0.4:46639 <socket.socket fd=7, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.2', 12345), raddr=('127.0.0.4', 46639)> -> b'server' <-The server is hanging on
r.readabove.Run the client in another shell, after starting the server:
$ python3 client.py A Connected from 127.0.0.3 <socket.socket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.3', 34135), raddr=('127.0.0.2', 12345)> <- b'server' -> b'client' Closed socket$ lsof -ni @127.0.0.2 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME python3 26692 conathan 6u IPv4 9992763 0t0 TCP 127.0.0.2:12345 (LISTEN) python3 26692 conathan 7u IPv4 9991149 0t0 TCP 127.0.0.2:12345->127.0.0.4:46639 (CLOSE_WAIT)Example session:
server.py ''andclient.py A================================================
Note that file descriptors are not reused on the server side now, and
nothing hangs.
Server:
Client:
$ python3 client.py A Connected from 127.0.0.3 <socket.socket fd=6, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.3', 37833), raddr=('127.0.0.2', 12345)> <- b'server' -> b'client' Closed socketBehavior for different combinations of closure modes
====================================================
Perhaps this is overkill, but here's what happens for all 15 possible
combinations of how the client and server close their connections.
For example, "Client=S, Server=S" below means we run
$ python3 client.py Sand$ python3 server.py S. Sometimes multiplecombinations have the same behavior, so they're grouped together
below.
Client hangs on
r.readon second connection, and killing the serveron the other end has no effect, with the socket stuck in CLOSE_WAIT on
the client side forever.
Client='', Server=S;
Client=A, Server=S
------------------
Server hangs on
r.readon second connection, and client exitsnormally, with the socket stuck in CLOSE_WAIT on the server side
forever.
Everything works the first time, but if you run the client in a loop,
e.g. with
then the server will eventually hang on
r.readafter ~3 clientsessions.
Client='', Server='';
Client='', Server=A;
Client=A, Server='';
Client=SA, Server=''
-------------------
Everything works! But here we see that the client and/or server
(whichever side is using '' for mode) is not reusing the socket file
descriptors right away (have to wait for GC). This is evidence that
the problem is due to stale state in asyncio tied to the reused file
descriptors.
Everything works, but this is not surprising because both sides closed
the
StreamWriterwithw.close().Possibly related bugs
=====================
https://bugs.python.org/issue43253: Windows only, calling
socket.socket.close() on a socket used with
asyncio.open_connection(sock=socket).
https://bugs.python.org/issue41317: reused file descriptors across
different connections.
https://bugs.python.org/issue34795,
https://bugs.python.org/issue30064: closing a socket used with
asyncio.
https://bugs.python.org/issue35065: reading from a closed stream can
hang. Could be related if the problem is due to aliased streams from
reused file descriptors.
https://bugs.python.org/issue43183: sockets used with asyncio getting
stuck in WAIT_CLOSED.
System info
===========
ntc2 commentedon Aug 2, 2021
Oh, and I can't count: there are 16 = 4x4 possible combinations of socket closure modes for the client and server. The one I missed was Client='', Server=SA, where everything works because the client doesn't reuse file descriptors and the server closes its sockets correctly.
ntc2 commentedon Oct 24, 2021
Just wanted to clarify: my previous "where everything works" comment is not saying this bug doesn't exist, I just mean I missed one case in my analysis of the bug. The bug is very much there, and easy to reproduce using the example programs I attached.
Bump!
kumaraditya303 commentedon Oct 3, 2022
@ntc2 Is this still an issue? This issue description is very large and is hard to understand what the problem is.
gvanrossum commentedon Oct 3, 2022
Since the OP has invested much effort in the analysis, maybe they can submit a PR using a fix? We're still short of asyncio experts and it's a lot easier to review a PR than it is to come up with a fix for a complex bug like this.
Also, the first step should be if this is still a problem with 3.11 (or the main branch).
ntc2 commentedon Oct 4, 2022
Yes, this is still an issue, I just tested against 6cbbc26. For example, the
python3 server.py Aandpython3 client.py Scombination fails as originally described.Sorry that the issue description is so large, I may have gotten carried away describing the test cases. But the high level description of the issue is not complex. I'll repeat it again here, trying to be more clear. Let me know if it still doesn't make sense.
Problem
The
asynciostreams API providesopen_connection, which lets you create a reader-writer stream pair(r,w)from an already connected socketsocket, like this:There are two different
closefunctions you can call when you're done with the stream --w.close()andsocket.close()-- and your program can hang if you usesocket.close()but notw.close(). In particular, the hang happens when the OS reuses the underlying file descriptor ofsocketin a new socketsocket2, you create a new stream with(r2,w2) = asyncio.open_connection(sock=socket2), and you then callr2.read(...). When the hang happens, the socketsocket2is stuck in theCLOSE_WAITstate.Example Programs Illustrating the Bug
There are simple example programs attached to the original bug report. You can reproduce the problem by running
python3 server.py Ain one shell followed bypython3 client.py Sin another shell.I don't know anything about how
asynciois implemented internally, so I have no idea how to fix this issue, but hopefully the simple example programs and ease of reproducing the bug will make it obvious to someone familiar with theasynciointernals. If no one who understands theasyncioimplementation is available I can take a stab at it myself.14 remaining items
GH-88968: Add notes about socket ownership transfers (GH-97936)
GH-88968: Add notes about socket ownership transfers (GH-97936)
gvanrossum commentedon Oct 6, 2022
It's perhaps possible to call
self._ensure_fd_no_transport(sock)increate_connection(), except the latter lives inBaseEventLoop, while_ensure_fd_no_transport()is defined in a subclass thereof,BaseSelectorEventLoop. We can probably make this work by overridingcreate_connection()in the latter. Or maybe the check can be moved to_create_connection_transport().ntc2 commentedon Oct 6, 2022
I looked at your doc changes in #97936. In conjunction with a defensive "fail fast" fix, like making sure that
_ensure_fd_no_transport(sock)is always called as part ofasyncio.open_connection(sock=socket), the doc changes seem sufficient. But if for some reason it's not possible to ensure that_ensure_fd_no_transportis always called, and so the possibility of the more subtle failure mode remains (hanging later when the socket fd is reused), then I think the warning in the docs should be more harsh and explicitly tell the user that callingsocket.close()after the ownership transfer may lead to subtle bugs in their program.Anyway, thanks for debugging this! Making sure that
_ensure_fd_no_transportis always called as part ofopen_connectionseems like a great solution from the user friendliness point of view, and would have saved me hours of debugging when I ran into this problem.Merge branch 'main' into dictwatch
GH-88968: Reject socket that is already used as a transport (#98010)
gvanrossum commentedon Oct 7, 2022
I consider this good enough.
Merge branch 'main' into typewatch
Merge branch 'main' into typewatch
pythonGH-88968: Add notes about socket ownership transfers (python#97936
pythonGH-88968: Reject socket that is already used as a transport (py…
GH-88968: Add notes about socket ownership transfers (GH-97936)