Uh oh!
There was an error while loading. Please reload this page.
Tweak dropped connection detection - #185
Conversation
From my own testing, this updated implementation seems to work fine: Server: importuvicornfromstarlette.responsesimportPlainTextResponseapp=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): importtimeimporthttpcorewithhttpcore.SyncConnectionPool() astransport:
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__inrange(2):
time.sleep(1)
assertnotstream.connection.is_connection_dropped()
# After 3s, Uvicorn shuts down the connection, and we detect it.time.sleep(1)
assertstream.connection.is_connection_dropped()
finally:
stream.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.
May you please add the same mechanism to the curio backend?
httpcore/httpcore/_backends/curio.py
Lines 135 to 138 in 685a9b6
lovelydinosaur
commented
Sep 14, 2020
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?....) |
agronholm
commented
Sep 14, 2020
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 |
florimondmanca
commented
Sep 14, 2020
Pretty interesting, indeed… >>>importsocket>>>alice, bob=socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
>>>alice.setblocking(False)
>>>bob.setblocking(False)
>>>alice.recv(1, socket.MSG_PEEK)
BlockingIOError: [Errno35] Resourcetemporarilyunavailable>>>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 |
florimondmanca
commented
Sep 14, 2020
@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? |
agronholm
commented
Sep 14, 2020
It just requires a lot of established connections within a single process, right? That shouldn't be hard to do. |
lovelydinosaur
commented
Sep 14, 2020
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 |
lovelydinosaur
commented
Sep 14, 2020
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) |
florimondmanca
commented
Sep 14, 2020
I guess we could roll with |
lovelydinosaur
commented
Sep 18, 2020
I'm all for it - it really does look like the actually most sensible thing to do. |
florimondmanca
commented
Sep 22, 2020
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 |
agronholm
commented
Sep 22, 2020
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. |
florimondmanca
commented
Sep 22, 2020
Yep, that's what I actually ended up using in this PR too… Updated #193 as well — looking quite good now! |
florimondmanca
commented
Oct 2, 2020
Closing in favor of #193, which I think is getting in good shape now! |
florimondmanca
commented
Oct 8, 2020
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 |
florimondmanca
commented
Oct 8, 2020
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
lovelydinosaur
commented
Oct 12, 2020
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:
|
florimondmanca
commented
Oct 17, 2020
Okay, I think this is starting to look quite good? Applied latest suggestions and addressed comments, happy to take new reviews! |
* 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
selectorsmodule, a "higher-level" alternative toselectthat 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.