-
Notifications
You must be signed in to change notification settings - Fork 105
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 support for socket options #668
Conversation
httpcore/backends/sync.py
Outdated
] | ||
|
||
|
||
def create_connection( |
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 function is very similar to the built-in create_connection
function, but it can also accept socket options.
We can't set socket options after a connection has been made because some options affect the connection itself, so we must set them before the connection was made.
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.
Okay, I'd suggest we add a comment noting that.
Perhaps rephrased to "This function is equivalent to Python's socket.create_connection()
function, but also accepts socket options." and linking to the source on GitHub?
If we want to support socket options for async interfaces, we need to change a lot of things. Currently, the 'get_asynclib' function is used to get a specific backend interface provided by anyio, then the "connect_tcp" and "connect_unix" asynchronous functions of that module are used to create a connection; how can we override these methods while keeping it as simple as possible? |
As a simple solution, we can associate each library with its function and then call that function. Old async def connect_unix(path: Union[str, "PathLike[str]"]) -> UNIXSocketStream:
path = str(Path(path))
return await get_asynclib().connect_unix(path) New async def connect_unix(path: Union[str, "PathLike[str]"]) -> UNIXSocketStream:
path = str(Path(path))
library = sniffio.current_async_library()
map = {
"trio": trio_connect_tcp,
"asyncio": asyncio_connect_tcp,
}
return map[library](path) |
It took me a little while to track down what you're referring to here.
We'd need to work this through on a case-by-case basis. The three cases to cover are We'd want the PR to start by adapting each of...
...and then work through from there. Let's link to each of these here so it's easier to review how we'd handle this... httpcore/httpcore/backends/sync.py Lines 94 to 96 in 82466e1
httpcore/httpcore/backends/trio.py Lines 127 to 129 in 82466e1
httpcore/httpcore/backends/asyncio.py Lines 111 to 115 in 82466e1
You've already demonstrated how we'd adapt the sync case. The I think you're pointing out that the asyncio-via-anyio case is actually really awkward to resolve. And yes, that looks correct to me. It does look like duplicating the anyio 🤔 |
Related, and possibly helpful here - it might be worthwhile taking a look at what |
There is an elegant solution, which is to ignore the socket option, which affects the connect method itself. Our issue is that the socket bind and connect methods behave differently depending on socket options, such as That is very fun, but trio does not support local address binding... There is also REUSEPORT, which allows you to listen to an IP port pair with multiple sockets, but it is far from httpcore because httpcore is a client library and does not support REUSEPORT at all. I looked for options that affect bind and connect methods and that httpcore or httpx users can use in their applications, but I couldn't find anything. I believe it is better to ignore such options and set options after the backend library has been created a connection. This is how the updated function could look. class Backend(AsyncNetworkBackend):
async def connect_tcp(
...,
socket_options
):
...
stream = await backend.connect_tcp(
remote_host=host,
remote_port=port,
local_host=local_address,
)
for option in socket_options:
stream._socket.setsockopt(*option)
return AsyncIOStream(stream) @tomchristie what are you think about this solution? |
Also, because we don't need options affecting bind or connect, we can use socket.create_connection instead of our newly created one to avoid code duplication |
Okay, setting socket options after the connect is probably(?) okay for our use-case. For the async case I'd suggest we push the implementation into |
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.
Looking good.
Let's also...
- Add this to the CHANGELOG.
- Include an update in the connection pool config docs
We should also take a call on if we really do want this, or if what we actually need is just a better default for the sync backed so that it includes See #651 (comment) |
I think we should make TCP_NODELAY the default, but also make socket options configurable; there are many options in unix sockets that developers can use, and this can be a powerful feature for developers who know what they're doing. Additionally, fixing encode/httpx#2635 requires these configurable sockets. which initially inspired me to open this PR :D |
httpcore/_sync/connection_pool.py
Outdated
@@ -109,6 +110,7 @@ def __init__( | |||
self._network_backend = ( | |||
SyncBackend() if network_backend is None else network_backend | |||
) | |||
self._socket_options = () if socket_options is None else socket_options |
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.
self._socket_options = () if socket_options is None else socket_options | |
self._socket_options = socket_options |
it looks to me like we should leave self._socket_options
as optional throughout.
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.
Should we always make sure to check if socket_options
is not None before using it?
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.
It would make sense for us to do the if socket_options is None
dance at the last possible point, in the network backends.
httpcore/_sync/connection.py
Outdated
@@ -54,6 +55,7 @@ def __init__( | |||
self._connection: Optional[ConnectionInterface] = None | |||
self._connect_failed: bool = False | |||
self._request_lock = Lock() | |||
self._socket_options = () if socket_options is None else socket_options |
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.
self._socket_options = () if socket_options is None else socket_options | |
self._socket_options = socket_options |
let's keep this optional just now.
@@ -187,7 +187,7 @@ def test_debug_request(caplog): | |||
( | |||
"httpcore.connection", | |||
logging.DEBUG, | |||
"connect_tcp.started host='example.com' port=80 local_address=None timeout=None socket_options=()", | |||
"connect_tcp.started host='example.com' port=80 local_address=None timeout=None socket_options=None", |
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, this is better.
docs/connection-pools.md
Outdated
@@ -79,6 +79,7 @@ The connection pool instance is also the main point of configuration. Let's take | |||
a particular address family. Using `local_address="0.0.0.0"` will | |||
connect using an `AF_INET` address (IPv4), while using `local_address="::"` | |||
will connect using an `AF_INET6` address (IPv6). | |||
* `socket_options`: Socket options that have to be included in the TCP socket when the connection was established. |
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.
let's be consistent with our placement of socket_options
in the list, so that the docs match the code.
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 good, yeah.
I've included some possible comments here. (?)
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Co-authored-by: Tom Christie <tom@tomchristie.com>
Closes #662