-
-
Notifications
You must be signed in to change notification settings - Fork 883
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
Add trio concurrency backend #276
Conversation
04de85b
to
c5d9552
Compare
🎉 Very exciting! |
79f626c
to
bf42b68
Compare
bf42b68
to
0fd803f
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 is great, I now have an excuse to look into Trio 😄
Left some comments and a question.
0fd803f
to
4f30eea
Compare
Can you verify if Trio properly doesn't sent SNI when it receives an IP address? If it does we need to filter that out within our backend implementation. If Trio has the proper behavior we can close #278 and add some documentation for future users / backend implementations. |
Unrelated to this PR: but can we change all instances of |
Yup, good idea @sethmlarson. As you mentioned this can (should?) be implemented in a separate PR. :-) |
I'm encountering a bug when performing a real-life HTTP/2 + TLS request with the # example.py
import trio
import httpx
from httpx.concurrency.trio import TrioBackend
async def main(url):
async with httpx.AsyncClient(backend=TrioBackend()) as client:
await client.get(url)
trio.run(main, "https://example.com") $ python example.py
Traceback (most recent call last):
File "example.py", line 11, in <module>
trio.run(main, "https://example.com")
File "/Users/florimond/Developer/python-projects/httpx/venv/lib/python3.7/site-packages/trio/_core/_run.py", line 1783, in run
raise runner.main_task_outcome.error
File "example.py", line 8, in main
await client.get(url)
File "/Users/florimond/Developer/python-projects/httpx/httpx/client.py", line 363, in get
trust_env=trust_env,
File "/Users/florimond/Developer/python-projects/httpx/httpx/client.py", line 610, in request
trust_env=trust_env,
File "/Users/florimond/Developer/python-projects/httpx/httpx/client.py", line 181, in send
allow_redirects=allow_redirects,
File "/Users/florimond/Developer/python-projects/httpx/httpx/client.py", line 218, in send_handling_redirects
request, verify=verify, cert=cert, timeout=timeout
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/connection_pool.py", line 120, in send
raise exc
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/connection_pool.py", line 115, in send
request, verify=verify, cert=cert, timeout=timeout
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/connection.py", line 57, in send
response = await self.h2_connection.send(request, timeout=timeout)
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/http2.py", line 45, in send
status_code, headers = await self.receive_response(stream_id, timeout)
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/http2.py", line 119, in receive_response
event = await self.receive_event(stream_id, timeout)
File "/Users/florimond/Developer/python-projects/httpx/httpx/dispatch/http2.py", line 158, in receive_event
await self.stream.write(data_to_send, timeout)
File "/Users/florimond/Developer/python-projects/httpx/httpx/concurrency/trio.py", line 86, in write
await self.stream.send_all(data)
File "/Users/florimond/Developer/python-projects/httpx/venv/lib/python3.7/site-packages/trio/_ssl.py", line 684, in send_all
with self._outer_send_conflict_detector:
File "/Users/florimond/Developer/python-projects/httpx/venv/lib/python3.7/site-packages/trio/_util.py", line 122, in __enter__
raise trio.BusyResourceError(self._msg)
trio.BusyResourceError: another task is currently sending data on this SSLStream Everything is fine if I force The place where this error is raised alternates between But, quoting the description of
And there's no way around it — probably a way for Trio to tell us to do things differently. …This is where the "read/write for a while" approach that @tomchristie mentioned here as an alternative to Recap: from my understanding, right now the Trio backend can't be made compatible with HTTP/2, and we need to rework the sending/reception of request/response data to unblock this. Since HTTP/1.1 is working though, what would be everyone's thoughts on transparently disabling HTTP/2 on trio for now temporarily, so we can get Trio + HTTP/1.1 in, and add HTTP/2 support later? (I managed to force usage of HTTP/1.1 by executing: ssl_context.set_alpn_protocols(["h1"])
ssl_context.set_npn_protocols(["h1"]) before opening the |
3b1f99c
to
7dfdaa3
Compare
7dfdaa3
to
c73a063
Compare
@florimondmanca That makes me think that our asyncio implementation might have the same issue? Maybe we need to revisit the concurrency issue for HTTP/2 @tomchristie? |
@sethmlarson Yes, if asyncio does not have guards in place it’s very possible that we’re writing to the stream writer at the same time as well, potentially sending corrupted data (although up to now it doesn’t seem to have surfaced?). |
c983bbc
to
a7879e6
Compare
I pushed a commit demonstrating reversing the way we do things: serving the server in a separate thread, which allows us to do all async stuff in the main thread and a "native" async environment. (I've made pytest share the server during the whole test session, so as a benefit tests are now super fast since we don't need to set it up/tear it down on every test. Also we don't need to run sync tests in the threadpool anymore.) The commit also turns the server restart into a no-op to prove it has no effect on the tests. There's still a failing tests for the case where an exception is raised within the ASGI app. The failure might come from how we're handling that case — for now we don't feed the app exception back into the background manager in |
So the reasoning there was because If we can ensure that it's not async then we know that'll be the case since we can't task switch around it (an alternative would be to introduce a lock around it.) |
Would there be any sense in creating a pull request that only does that, without the rest of this, so that we can get that into the test suite and confirm it, independently of the trio work? |
43b6042
to
fb95be2
Compare
So, I think I got confused about what a readable socket means — which means the implementation originally pushed last week was correct. I added a bunch of code comments along with the last commit to clarify the asyncio and trio implementations of If all looks well, I'd love a new review from @sethmlarson, and @tomchristie's approval for merging this. (I know there were past discussions about whether trio support should land in core, or be released as a third-party package. My position is that we should merge this in core first, and then possibly extract it into a separate package once we have enough live experience.) |
a4985fb
to
6af585a
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 looks great! 🎉
ef50c15
to
7ae40b8
Compare
Now that 0.7.3 is out you ready to merge this? |
Yup, if this sounds good to you then let's merge. I'll add docs in a subsequent PR. :) |
It's happening!!! 💥 |
What an amazing mountain of work, thank you @florimondmanca and @tomchristie, @pgjones, @yeraydiazdiaz, @pquentin, and @njsmith for reviewing! 🎉 |
Thanks to everyone who reviewed and commented! 🌟 |
Fixes #120
Current status: we're blocked by the connection pool implicitly relying on asyncio polling sockets in the background to tell if they're still readable. See discussion around #276 (comment).
Most of the notes below are outdated.
Getting the tests to pass on this one ended up having to tackle more tricky issues than I initially thought. (There's still
start_tls()
to figure out, but that will be for a future PR.)So, a few notes, from the most straight-forward to the most obscure:
HTTP2Dispatcher.initiate_connection()
into an async function; it allowed to remove the need forwrite_no_block()
on the concurrency backend (which trio does not have an obvious equivalent for). Things seem to be okay, but if I missed any reasons why it was not async in the first place, let me know! cc @tomchristietest_connections.py
— otherwise they'd stall when using Trio, probably because the underlying TCP stream would be waiting to be closed. I'm not 100% sure I understand the origin of this, though. Anyway, I ended up converting all those tests to useasync with
for consistency.If it can help with reviewing, I'd be happy to
pytest.skip()
some tests in this PR and then submit solutions for the tricky parts in separate PRs that resolve those skips. Let me know how you'd like to handle this. :-)