Uh oh!
There was an error while loading. Please reload this page.
Support async cancellations - #719
Conversation
001de05 to
dceaaf6Compare| await pool.request( | ||
| "GET", "http://example.com" | ||
| ) | ||
| assert not pool.connections |
There was a problem hiding this comment.
I don't think this is quite correct.
The behaviour we'd want to see here is that there is a connection in the pool, but it is IDLE.
There was a problem hiding this comment.
I thought about it and can't remember when, but I remember thinking that this is much safer.
There was a problem hiding this comment.
Ah yes, there's some complexities here... 🤔
Either way, this test case is a great starting point.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
karpetrosyan
commented
Jun 12, 2023
First, we must decide how we will support "CancelScope" while keeping sync and async interfaces similar. I'm not sure whether it's better to do it in another PR or in this one. |
lovelydinosaur
commented
Jun 14, 2023
Okay. Before we get to implementation, could you start off by showing me where we need shielding rather than how we're going to implement it? In order to have confidence that we're really getting it right I'd be interested in looking at that from the ground up... Suppose we're just working directly with an |
karpetrosyan
commented
Jun 14, 2023
httpcore/httpcore/_async/http11.py Lines 220 to 232 in eb5957d This function appears to be the main source of the problem; the issue here is that if we call this method while cancelled, the resources will not be closed. |
Perhaps we should add a test case at that level of the API? (Resolve cancellation shielding for an individual connection, and be confident we've got that sorted before moving up the stack?) |
karpetrosyan
commented
Jun 14, 2023
Sounds good |
…tpcore into support-async-cancellations
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| @pytest.mark.anyio | ||
| async def test_h11_response_closed(): | ||
| origin = httpcore.Origin(b"http", b"example.com", 80) | ||
| stream = SlowStream() | ||
| async with httpcore.AsyncHTTP11Connection(origin, stream) as conn: | ||
| with anyio.move_on_after(0.001): | ||
| await conn.request("GET", "http://example.com") | ||
| assert conn.is_closed() | ||
| @pytest.mark.anyio | ||
| async def test_h2_response_closed(): | ||
| origin = httpcore.Origin(b"http", b"example.com", 80) | ||
| stream = SlowStream() | ||
| async with httpcore.AsyncHTTP2Connection(origin, stream) as conn: | ||
| with anyio.move_on_after(0.001): | ||
| await conn.request("GET", "http://example.com") | ||
| assert conn.is_closed() |
There was a problem hiding this comment.
Okay these are looking great - good starting point for us working through this really comprehensively.
There was a problem hiding this comment.
If we're done with failing tests, I believe we should decide how and where we want to see shield logic.
Okay, so based on the HTTP/1.1 test case I've got a nice clear example of the existing behaviour... Here's the behaviour we see when we get a network error during a write operation on an HTTP/1.1 connection: importtrioimporthttpcoreclassSlowStream(httpcore.AsyncNetworkStream):
asyncdefwrite(self, buffer, timeout=None):
raisehttpcore.WriteError()
asyncdefaclose(self):
passasyncdefmain():
origin=httpcore.Origin(b"http", b"example.com", 80)
stream=SlowStream()
asyncwithhttpcore.AsyncHTTP11Connection(origin, stream) asconn:
try:
awaitconn.request("GET", "http://example.com")
excepthttpcore.NetworkError:
passprint(conn)
# <AsyncHTTP11Connection ['http://example.com:80', CLOSED, Request Count: 1]>In contrast, here's the behaviour we see when we get a cancellation exception during a write operation on an HTTP/1.1 connection: importtrioimporthttpcoreclassSlowStream(httpcore.AsyncNetworkStream):
asyncdefwrite(self, buffer, timeout=None):
awaittrio.sleep(1)
asyncdefaclose(self):
passasyncdefmain():
origin=httpcore.Origin(b"http", b"example.com", 80)
stream=SlowStream()
asyncwithhttpcore.AsyncHTTP11Connection(origin, stream) asconn:
withtrio.move_on_after(0.001):
awaitconn.request("GET", "http://example.com")
print(conn)
# <AsyncHTTP11Connection ['http://example.com:80', ACTIVE, Request Count: 1]>trio.run(main) |
Starting with the HTTP/1.1 case it looks to me like we want...
exceptBaseExceptionasexc:
withShieldFromCancellation():
asyncwithTrace("response_closed", logger, request) astrace:
awaitself._response_closed()
raiseexc
exceptBaseExceptionasexc:
# If we get an exception while streaming the response,# we want to close the response (and possibly the connection)# before raising that exception.withShieldFromCancellation():
awaitself.aclose()
raiseexcI'm not presuming how we implement the (Based on |
karpetrosyan
commented
Jun 15, 2023
To me, it appears to be completely correct. |
Are you referring to slow-write test for |
I'm suggesting that we'll also want slow-read tests. Here's how that'd look for the HTTP/1.1 case... classSlowReadStream(httpcore.AsyncNetworkStream):
def__init__(self, buffer):
self.buffer=bufferasyncdefwrite(self, buffer, timeout=None):
passasyncdefread(self, max_bytes: int, timeout: typing.Optional[float] =None) ->bytes:
ifnotself._buffer:
awaittrio.sleep(1)
else:
returnself._buffer.pop(0)
asyncdefaclose(self):
passstream=SlowReadStream([
b"HTTP/1.1 200 OK\r\n",
b"Content-Type: plain/text\r\n",
b"Content-Length: 1000\r\n",
b"\r\n",
b"Hello, world!...", # The stream will hang after this portion of the incomplete response,# so that we're able to test our async cancellation semantics.
]) |
karpetrosyan
commented
Jun 15, 2023
Is a buffer really necessary? |
lovelydinosaur
commented
Jun 15, 2023
You'd need this or something like this in order to test the cancellation handling for |
lovelydinosaur
commented
Jun 15, 2023
Okay, want to try...
|
Uh oh!
There was an error while loading. Please reload this page.
lovelydinosaur
commented
Jun 26, 2023
Thanks so much for your work on this. |
Problem
Cancellations in the middle of a request/response cycle are not supported by the httpcore.
Something like this should brake the connection pool
TODO