-
Notifications
You must be signed in to change notification settings - Fork 109
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
Tweak dropped connection detection #185
Conversation
From my own testing, this updated implementation seems to work fine: Server: import uvicorn
from starlette.responses import PlainTextResponse
app = PlainTextResponse("Hello, World!")
if __name__ == "__main__":
# Make it so that idle client connections are shut down after 3s.
uvicorn.run(app, timeout_keep_alive=3) Client script (exits successfully): import time
import httpcore
with httpcore.SyncConnectionPool() as transport:
method = b"HEAD"
url = (b"http", b"localhost", 8000, b"/")
headers = [(b"host", b"localhost")]
*_, stream = transport.request(method, url, headers)
try:
# During the first two seconds, the connection remains open…
for __ in range(2):
time.sleep(1)
assert not stream.connection.is_connection_dropped()
# After 3s, Uvicorn shuts down the connection, and we detect it.
time.sleep(1)
assert stream.connection.is_connection_dropped()
finally:
stream.close() |
httpcore/_backends/sync.py
Outdated
@@ -78,8 +78,8 @@ def close(self) -> None: | |||
self.sock.close() | |||
|
|||
def is_connection_dropped(self) -> bool: | |||
rready, _wready, _xready = select.select([self.sock], [], [], 0) | |||
return bool(rready) | |||
rready = wait_for_read([self.sock], timeout=0) |
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.
May you please add the same mechanism to the curio
backend?
httpcore/httpcore/_backends/curio.py
Lines 135 to 138 in 685a9b6
def is_connection_dropped(self) -> bool: | |
rready, _, _ = select.select([self.socket.fileno()], [], [], 0) | |
return bool(rready) |
Seems like a clear win yup. We'll probably also want some follow ups, eg... if no selector exists, then our graceful-ish fallback can just be never detecting connections as expired, and just relying on the keepalive expiry. There might be some neater behaviour we can have for detecting expiry, at the point we're actually making the request. (Eg. attempting a read with a minimal timeout prior to writing any data, in order to see any EOF if it exists?....) |
I find it worrying that there is no test that verifies that dropped connections are actually detected. The test suite passes just fine if I unconditionally return |
It'll come. 😀 |
Anyway, I did some manual testing and found that
Given that |
Pretty interesting, indeed… >>> import socket
>>> alice, bob = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
>>> alice.setblocking(False)
>>> bob.setblocking(False)
>>> alice.recv(1, socket.MSG_PEEK)
BlockingIOError: [Errno 35] Resource temporarily unavailable
>>> bob.send(b"hello")
>>> alice.recv(5)
b'hello'
>>> bob.close() # Server connection lost
>>> alice.recv(1, socket.MSG_PEEK)
b'' |
Following on @tomchristie's comment, perhaps this Also, it's not clear to me yet whether #182 would surface on Windows. Eg in urllib3/urllib3#589 @sethmlarson wrote:
I'm not sure whether this implies that the problem from #182 isn't present on Windows, or if Seth meant that it would be solved with another urllib3 PR at the time. If Windows is not a problem, then I guess either approach is fine, though I'm more keen on relying on a well-documented and standard solution like |
@agronholm Since you mentioned testing on Windows, were you / would you be able to reproduce the issue in #182 on Windows? If we can't reproduce there, then perhaps the safest bet would be to just not deal with it at the moment, issue a fix for Unix, and then deal with any upcoming issues for Windows should they arrive? |
It just requires a lot of established connections within a single process, right? That shouldn't be hard to do. |
It looks like MSG_PEEK is possibly a better approach anyways, seems to be available throughout, and less likely to have platform specific differences vs. working with |
Incidentally we might want to rename If there's data still in the buffer we want the interface to return False. (We'll most likely end up with a protocol error in that odd case, at least for HTTP/1.1, but the point is that reading on the socket isn't at EOF, so we should go ahead an make the request) |
I guess we could roll with |
I'm all for it - it really does look like the actually most sensible thing to do. |
Hitting some road blocks on trying to implement this
E TypeError: transport sockets do not support recv() method
Filed #193 if folks would like to see the code. I'm going to proceed and see if the |
I recall running into this myself. If memory serves, I got the
I'll remind you that this does not fix the problem on Windows. |
Yep, that's what I actually ended up using in this PR too… Updated #193 as well — looking quite good now! |
Closing in favor of #193, which I think is getting in good shape now! |
I am reviving this PR based on the insights from Nathaniel in #193 (comment) TL;DR: this should only be a problem on Unix, so the |
Okay, so that one thing I hadn't fully groked about this is that it shouldn't be called Why? Because the only valid data that could be about to come back from the socket at that point in the connection lifecycle is But in general the state of So... I think we need to:
|
Okay, I think this is starting to look quite good? Applied latest suggestions and addressed comments, happy to take new reviews! |
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.
Looks great to me 🍰
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.
Thats perfect for me 🔥
* Tweak sync dropped connection detection * Nits * Use common helper across sync, asyncio, curio, anyio * Update * Add test case * Rename is_connection_dropped -> is_socket_readable, tweak comments * Switch to stealing util from trio Co-authored-by: Tom Christie <tom@tomchristie.com>
Closes #182
urllib3 had to deal with an issue similar to #182 a while ago: urllib3/urllib3#589
Their approach was to backport a bunch of 3.5+ logic to maintain 2.7 compatibility, but we're lucky enough to be able to use that logic from the stdlib in the form of the
selectors
module, a "higher-level" alternative toselect
that handles cross-platform subtleties among other things.This PR switches the sync backend to use
selectors.select()
instead ofselect.select()
for detecting whether a connection was dropped (i.e. whether the underlying socket has become immediately readable).Confirmed that this solves #182, since the CI build passes here but fails on #219.