-
-
Notifications
You must be signed in to change notification settings - Fork 932
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
Replace task cancellation in BaseHTTPMiddleware
with http.disconnect
+recv_stream.close
#1715
Replace task cancellation in BaseHTTPMiddleware
with http.disconnect
+recv_stream.close
#1715
Conversation
a287b8d
to
c98203e
Compare
@jhominal Hey, thanks a lot for this. I think I would be very interested in seeing what the tests look like. Might help me better grok what practical situations this PR would help address. About:
This came from what I would call an interpretation of the spec, which reads (emphasis mine): https://asgi.readthedocs.io/en/latest/specs/www.html#request-receive-event
I interpret "you should consume as long as So, as for...
My interpretation (although the spec doesn't mention any notion of "cancellation") would be, no. As for https://asgi.readthedocs.io/en/latest/specs/www.html#disconnect-receive-event
Clearly, we are in the second situation ("if receive is called after a response has been set"), because we now have this: starlette/starlette/middleware/base.py Lines 120 to 121 in c98203e
So the answer to...
Seems to be: yes, the We might want to make this even clearer by renaming the |
@florimondmanca Thank you for your reply! I am going to work on the test cases - at least a few of them will be similar to existing issues/pull requests. But yes, it is my belief that this proposal has the potential to be an alternative to #1710, #1700 (which fixes the "background tasks are canceled" issue by pushing them outside of the response processing), #1699, #1441 (which was closed yesterday). I believe it would fix at least #1438, which is the issue I raised a little time ago. |
f1d1e21
to
c77c5eb
Compare
This does look very nice, I do think removing the cancellation is a step in the right direction.
Did you identify any issues with that proposal so that we can have them in a comment for posteriority / we can close that PR? |
I will just comment on the two issues that I believe are still open and would be directly affected by this PR:
|
I tried to add a test case for this in starlette/tests/middleware/test_base.py Lines 265 to 293 in f5cb1ee
The original issue says "subsequent ones are not processed until the 10 second sleep has finished (the first request returns before then though)". I was not able to reproduce that specific outcome, but I was able to reproduce the BackgroundTask not running when there was a BaseHTTPMiddleware present. I think it is the same bug, the behavior has just changed slightly over time. |
@adriangb: I have looked at your test cases while implementing mine. However, after porting and reviewing a copy of the #919 test, I As some of the issues reported on #919 were at a time when Starlette was implemented directly on top of |
1722e94
to
243d2ce
Compare
Hmm good point, looking at my test now I think that may be the case. I'll dig deeper tomorrow, thank you for combing through them. |
I just reread your test for #1438 and I realize that actually, it has a difference (which is that the background task waits on the |
BaseHTTPMiddleware
with http.disconnect
+recv_stream.close
BaseHTTPMiddleware
with http.disconnect
+recv_stream.close
243d2ce
to
137776b
Compare
I have just added a test to check for the issue reported by @kissgyorgy on #1678 (comment) - about the way that Fixing this test did not require any modification to the PR's modifications of |
@florimondmanca As you previously expressed interest in this PR, I chose to request you as a reviewer. Please feel free to suggest anyone else who you think should also take a look at this. |
I agree that this PR is languishing a bit, so I have decided to request review from all encode maintainers, in the hope that more people |
1062843
to
4c50738
Compare
4c50738
to
274bdeb
Compare
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.
This seems sensible to me. @Kludex and @florimondmanca I would like you two to take a look as well to see if you can spot any issues. Thank you @jhominal !
async with anyio.create_task_group() as task_group: | ||
|
||
async def wrap(func: typing.Callable[[], typing.Awaitable[T]]) -> T: | ||
result = await func() | ||
task_group.cancel_scope.cancel() | ||
return result | ||
|
||
task_group.start_soon(wrap, response_sent.wait) | ||
message = await wrap(request.receive) |
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.
What this is doing is saying "wait for a message from the client but if response_sent
gets set in the meantime then stop waiting/reading from the client and move on"
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.
Yes, the issue that I want to solve here is, if the downstream app is waiting on receive
, but as the response is sent (likely by the middleware), the downstream app cannot send
anything meaningfully, so there is no point in letting downstream wait for another message from upstream.
We could also choose to rely on upstream receive
returning a http.disconnect
message when the response is sent (which should happen), but when I wrote that bit, I thought that a belt-and-braces approach would be better.
However, that approach does mean that every call to receive
from an app gets an intermediary anyio.TaskGroup
for each BaseHTTPMiddleware
in the middleware chain.
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.
In other words, I would be open to modifying that part to remove the receive
wrapper to avoid that cost if it thought to be too much.
I'll check tomorrow. I've added this to the checklist for the next release. |
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.
🙏
I'll let you merge @jhominal 👍 Thanks for this, and sorry for taking long to review. Beautiful code. Well thought. 👍 |
I'll merge this, since it's the only PR missing for the release. |
The 0.21 release resolves a frequent error on our fastapi version. See: encode/starlette#1710 encode/starlette#1715
* Temporarily use fork for starlette 0.21 release The 0.21 release resolves a frequent error on our fastapi version. See: encode/starlette#1710 encode/starlette#1715 * Disable FTP as function app deploy option Security controls * Trace request attributes before invoking middleware If an exception is raised in subsequent middlewares, added trace attributes will still be logged to Azure. This allows us to find requests that fail in the logs. * Make config cache thread safe cachetools cache is not thread safe and there were frequent exceptions logged indicating that cache updates during async calls were failing with key errors similar to those described in: tkem/cachetools#80 Add a lock per table instance synchronizes cache updates across threads in. * Lint * Changelog
…ct`+`recv_stream.close` (#1715) * replace BaseMiddleware cancellation after request send with closing recv_stream + http.disconnect in receive fixes #1438 * Add no cover pragma on pytest.fail in tests/middleware/test_base.py Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> * make http_disconnect_while_sending test more robust in the face of scheduling issues * Fix issue with running middleware context manager Reported in #1678 (comment) Co-authored-by: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Co-authored-by: Marcelo Trylesinski <marcelotryle@gmail.com>
Partial revert of this commit: 040d8c8 Replace task cancellation in `BaseHTTPMiddleware` with `http.disconnect`+`recv_stream.close` (encode#1715)
Partial revert of this commit: 040d8c8 Replace task cancellation in `BaseHTTPMiddleware` with `http.disconnect`+`recv_stream.close` (encode#1715)
Partial revert of this commit: 040d8c8 Replace task cancellation in `BaseHTTPMiddleware` with `http.disconnect`+`recv_stream.close` (encode#1715)
Partial revert of this commit: 040d8c8 Replace task cancellation in `BaseHTTPMiddleware` with `http.disconnect`+`recv_stream.close` (encode#1715)
There have been various issues related to the way that
BaseHTTPMiddleware
works, and in particular, related to the way that it uses task cancellation in order to force the downstream ASGI app to shut down.In particular, I would like to highlight that:
BaseHTTPMiddleware
, cancellation is used in order to get out of a deadlock between a call tosend_stream.send
that never finishes and blocks a downstreamapp
from completing, and a call torecv_stream.receive
that is never made because thebody_stream
iterator has been orphaned by thedispatch
method implementation;asyncio
andtrio
have very different cancellation semantics, that may be the only practical position that ASGI can take;I believe that this pull request outlines a solution that avoids both using task cancellation on the downstream ASGI application, and also avoids deadlocking the downstream ASGI application. It does that by replacing task cancellation with the following features:
anyio.Event
app_disconnected
is set, that triggers the following consequences:receive
function that is passed to the downstream ASGI application so that, ifapp_disconnected
is set, a message with typehttp.disconnect
will be returned (instead of waiting for the next message fromrequest.receive
);recv_stream
, which has the effect of removing the root cause of the known deadlock issue. However, assend_stream.send
raises ananyio.BrokenResourceError
in that case, we need to wrapsend_stream.send
before passing it to the downstream ASGI application;What are the consequences of that proposed change?
BaseHTTPMiddleware
is found in an ASGI middleware chain, and that is completely unexpected by ASGI applications (even Starlette's own response classes do not take that possibility in account), that may not be an actual loss of functionality;{"type":"http.disconnect"}
message, we allow ASGI applications that listen to that event (such as Starlette'sStreamingResponse
) to potentially wrap up their processing once it becomes clear that their work will be discarded and start cleanup and finalization (e.g. something may be added toFileResponse
);recv_stream
once the response has been completely sent, we remove the root cause of the deadlock, and ensure that it cannot happen even e.g. in the face of a fully cancellation-shielded application;There are two larger points that concern conformance to the ASGI specification / expected behavior (however, I do not know even with whom we could raise these points):
{"type":"http.disconnect"}
, in order to signal to the application "Your link with the HTTP client has been severed, there is no need to continue producinghttp.response.body
messages", is semantically compatible with what ASGI specifies?This PR is a draft because, as I did not have validation from the maintainers to propose such a change, I did not add the tests that would prove that various issues (some of them the same as those fixed by the many other PRs on the same subject) are actually fixed by my proposal. In case any maintainer expresses interest in actually trying to integrate this proposal, I will gladly work to complete this PR with the necessary tests.This PR is not a draft anymore, and:
BaseHTTPMiddleware
#919