-
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
Connection pool optimization: move socket polling from expiry checks to connection usage #928
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -240,7 +240,7 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]: | |
closing_connections = [] | ||
|
||
# First we handle cleaning up any connections that are closed, | ||
# have expired their keep-alive, or surplus idle connections. | ||
# have expired, or surplus idle connections. | ||
for connection in list(self._connections): | ||
if connection.is_closed(): | ||
# log: "removing closed connection" | ||
|
@@ -267,15 +267,12 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]: | |
for connection in self._connections | ||
if connection.can_handle_request(origin) and connection.is_available() | ||
] | ||
idle_connections = [ | ||
connection for connection in self._connections if connection.is_idle() | ||
] | ||
|
||
# There are three cases for how we may be able to handle the request: | ||
# | ||
# 1. There is an existing connection that can handle the request. | ||
# 2. We can create a new connection to handle the request. | ||
# 3. We can close an idle connection and then create a new connection | ||
# 3. We can close an idle/expired connection and then create a new connection | ||
# to handle the request. | ||
if available_connections: | ||
# log: "reusing existing connection" | ||
|
@@ -286,15 +283,19 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]: | |
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
elif idle_connections: | ||
# log: "closing idle connection" | ||
connection = idle_connections[0] | ||
self._connections.remove(connection) | ||
closing_connections.append(connection) | ||
# log: "creating new connection" | ||
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
else: | ||
purged_connection = next( | ||
(c for c in self._connections if c.is_idle() or c.has_expired()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also considers expired connections here for removal. Because A) clock may have moved forward so that some are now expired here B) expired ones are the ones that maybe have been disconnected by the server (they are not idle) |
||
None, | ||
) | ||
if purged_connection is not None: | ||
# log: "closing idle connection" | ||
self._connections.remove(purged_connection) | ||
closing_connections.append(purged_connection) | ||
# log: "creating new connection" | ||
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
|
||
return closing_connections | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ | |
ConnectionNotAvailable, | ||
LocalProtocolError, | ||
RemoteProtocolError, | ||
ServerDisconnectedError, | ||
WriteError, | ||
map_exceptions, | ||
) | ||
|
@@ -45,6 +46,7 @@ class HTTPConnectionState(enum.IntEnum): | |
ACTIVE = 1 | ||
IDLE = 2 | ||
CLOSED = 3 | ||
SERVER_DISCONNECTED = 4 | ||
|
||
|
||
class AsyncHTTP11Connection(AsyncConnectionInterface): | ||
|
@@ -59,7 +61,7 @@ def __init__( | |
) -> None: | ||
self._origin = origin | ||
self._network_stream = stream | ||
self._keepalive_expiry: Optional[float] = keepalive_expiry | ||
self._keepalive_expiry = keepalive_expiry | ||
self._expire_at: Optional[float] = None | ||
self._state = HTTPConnectionState.NEW | ||
self._state_lock = AsyncLock() | ||
|
@@ -76,13 +78,7 @@ async def handle_async_request(self, request: Request) -> Response: | |
f"to {self._origin}" | ||
) | ||
|
||
async with self._state_lock: | ||
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): | ||
self._request_count += 1 | ||
self._state = HTTPConnectionState.ACTIVE | ||
self._expire_at = None | ||
else: | ||
raise ConnectionNotAvailable() | ||
await self._update_state() | ||
|
||
try: | ||
kwargs = {"request": request} | ||
|
@@ -142,6 +138,29 @@ async def handle_async_request(self, request: Request) -> Response: | |
await self._response_closed() | ||
raise exc | ||
|
||
async def _update_state(self) -> None: | ||
async with self._state_lock: | ||
# If the HTTP connection is idle but the socket is readable, then the | ||
# only valid state is that the socket is about to return b"", indicating | ||
# a server-initiated disconnect. | ||
server_disconnected = ( | ||
self._state == HTTPConnectionState.IDLE | ||
and self._network_stream.get_extra_info("is_readable") | ||
) | ||
Comment on lines
+146
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this check be also done in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think yes, but cc @tomchristie who knows better about http2 |
||
if ( | ||
server_disconnected | ||
or self._state == HTTPConnectionState.SERVER_DISCONNECTED | ||
): | ||
self._state = HTTPConnectionState.SERVER_DISCONNECTED | ||
raise ServerDisconnectedError() | ||
|
||
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE): | ||
self._request_count += 1 | ||
self._state = HTTPConnectionState.ACTIVE | ||
self._expire_at = None | ||
else: | ||
raise ConnectionNotAvailable() | ||
|
||
# Sending the request... | ||
|
||
async def _send_request_headers(self, request: Request) -> None: | ||
|
@@ -279,18 +298,13 @@ def is_available(self) -> bool: | |
return self._state == HTTPConnectionState.IDLE | ||
|
||
def has_expired(self) -> bool: | ||
now = time.monotonic() | ||
keepalive_expired = self._expire_at is not None and now > self._expire_at | ||
|
||
# If the HTTP connection is idle but the socket is readable, then the | ||
# only valid state is that the socket is about to return b"", indicating | ||
# a server-initiated disconnect. | ||
server_disconnected = ( | ||
self._state == HTTPConnectionState.IDLE | ||
and self._network_stream.get_extra_info("is_readable") | ||
) | ||
if self._state == HTTPConnectionState.SERVER_DISCONNECTED: | ||
# Connection that is disconnected by the server is considered expired. | ||
# Pool then cleans up this connection by closing it. | ||
return True | ||
|
||
return keepalive_expired or server_disconnected | ||
now = time.monotonic() | ||
return self._expire_at is not None and now > self._expire_at | ||
|
||
def is_idle(self) -> bool: | ||
return self._state == HTTPConnectionState.IDLE | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -240,7 +240,7 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]: | |
closing_connections = [] | ||
|
||
# First we handle cleaning up any connections that are closed, | ||
# have expired their keep-alive, or surplus idle connections. | ||
# have expired, or surplus idle connections. | ||
for connection in list(self._connections): | ||
if connection.is_closed(): | ||
# log: "removing closed connection" | ||
|
@@ -267,15 +267,12 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]: | |
for connection in self._connections | ||
if connection.can_handle_request(origin) and connection.is_available() | ||
] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto. |
||
idle_connections = [ | ||
connection for connection in self._connections if connection.is_idle() | ||
] | ||
|
||
# There are three cases for how we may be able to handle the request: | ||
# | ||
# 1. There is an existing connection that can handle the request. | ||
# 2. We can create a new connection to handle the request. | ||
# 3. We can close an idle connection and then create a new connection | ||
# 3. We can close an idle/expired connection and then create a new connection | ||
# to handle the request. | ||
if available_connections: | ||
# log: "reusing existing connection" | ||
|
@@ -286,15 +283,19 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]: | |
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
elif idle_connections: | ||
# log: "closing idle connection" | ||
connection = idle_connections[0] | ||
self._connections.remove(connection) | ||
closing_connections.append(connection) | ||
# log: "creating new connection" | ||
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
else: | ||
purged_connection = next( | ||
(c for c in self._connections if c.is_idle() or c.has_expired()), | ||
None, | ||
) | ||
if purged_connection is not None: | ||
# log: "closing idle connection" | ||
self._connections.remove(purged_connection) | ||
closing_connections.append(purged_connection) | ||
# log: "creating new connection" | ||
connection = self.create_connection(origin) | ||
self._connections.append(connection) | ||
pool_request.assign_to_connection(connection) | ||
|
||
return closing_connections | ||
|
||
|
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 seems like we could just find the first available one here?
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.
Oh, I just noticed that this has been split into separate PRs. 😅