Skip to content

Commit b19e879

Browse files
Code review fixes. Add safe guard check to pool
1 parent 27e73bb commit b19e879

File tree

9 files changed

+113
-111
lines changed

9 files changed

+113
-111
lines changed

httpcore/_async/connection_pool.py

+17-20
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
from .._backends.auto import AutoBackend
77
from .._backends.base import SOCKET_OPTION, AsyncNetworkBackend
8-
from .._exceptions import (
9-
ConnectionNotAvailable,
10-
ServerDisconnectedError,
11-
UnsupportedProtocol,
12-
)
8+
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
139
from .._models import Origin, Request, Response
1410
from .._synchronization import AsyncEvent, AsyncShieldCancellation, AsyncThreadLock
1511
from .connection import AsyncHTTPConnection
@@ -200,7 +196,7 @@ async def handle_async_request(self, request: Request) -> Response:
200196
response = await connection.handle_async_request(
201197
pool_request.request
202198
)
203-
except (ConnectionNotAvailable, ServerDisconnectedError):
199+
except ConnectionNotAvailable:
204200
# In some cases a connection may initially be available to
205201
# handle a request, but then become unavailable.
206202
#
@@ -244,7 +240,7 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]:
244240
closing_connections = []
245241

246242
# First we handle cleaning up any connections that are closed,
247-
# have expired their keep-alive, or surplus idle connections.
243+
# have expired, or surplus idle connections.
248244
for connection in list(self._connections):
249245
if connection.is_closed():
250246
# log: "removing closed connection"
@@ -271,15 +267,12 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]:
271267
for connection in self._connections
272268
if connection.can_handle_request(origin) and connection.is_available()
273269
]
274-
idle_connections = [
275-
connection for connection in self._connections if connection.is_idle()
276-
]
277270

278271
# There are three cases for how we may be able to handle the request:
279272
#
280273
# 1. There is an existing connection that can handle the request.
281274
# 2. We can create a new connection to handle the request.
282-
# 3. We can close an idle connection and then create a new connection
275+
# 3. We can close an idle/expired connection and then create a new connection
283276
# to handle the request.
284277
if available_connections:
285278
# log: "reusing existing connection"
@@ -290,15 +283,19 @@ def _assign_requests_to_connections(self) -> List[AsyncConnectionInterface]:
290283
connection = self.create_connection(origin)
291284
self._connections.append(connection)
292285
pool_request.assign_to_connection(connection)
293-
elif idle_connections:
294-
# log: "closing idle connection"
295-
connection = idle_connections[0]
296-
self._connections.remove(connection)
297-
closing_connections.append(connection)
298-
# log: "creating new connection"
299-
connection = self.create_connection(origin)
300-
self._connections.append(connection)
301-
pool_request.assign_to_connection(connection)
286+
else:
287+
purged_connection = next(
288+
(c for c in self._connections if c.is_idle() or c.has_expired()),
289+
None,
290+
)
291+
if purged_connection is not None:
292+
# log: "closing idle connection"
293+
self._connections.remove(purged_connection)
294+
closing_connections.append(purged_connection)
295+
# log: "creating new connection"
296+
connection = self.create_connection(origin)
297+
self._connections.append(connection)
298+
pool_request.assign_to_connection(connection)
302299

303300
return closing_connections
304301

httpcore/_async/http11.py

+24-21
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,7 @@ async def handle_async_request(self, request: Request) -> Response:
7878
f"to {self._origin}"
7979
)
8080

81-
async with self._state_lock:
82-
if self._state == HTTPConnectionState.SERVER_DISCONNECTED:
83-
raise ServerDisconnectedError()
84-
85-
# If the HTTP connection is idle but the socket is readable, then the
86-
# only valid state is that the socket is about to return b"", indicating
87-
# a server-initiated disconnect.
88-
server_disconnected = (
89-
self._state == HTTPConnectionState.IDLE
90-
and self._network_stream.get_extra_info("is_readable")
91-
)
92-
if server_disconnected:
93-
self._state = HTTPConnectionState.SERVER_DISCONNECTED
94-
raise ServerDisconnectedError()
95-
96-
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
97-
self._request_count += 1
98-
self._state = HTTPConnectionState.ACTIVE
99-
self._expire_at = None
100-
else:
101-
raise ConnectionNotAvailable()
81+
await self._update_state()
10282

10383
try:
10484
kwargs = {"request": request}
@@ -158,6 +138,29 @@ async def handle_async_request(self, request: Request) -> Response:
158138
await self._response_closed()
159139
raise exc
160140

141+
async def _update_state(self) -> None:
142+
async with self._state_lock:
143+
# If the HTTP connection is idle but the socket is readable, then the
144+
# only valid state is that the socket is about to return b"", indicating
145+
# a server-initiated disconnect.
146+
server_disconnected = (
147+
self._state == HTTPConnectionState.IDLE
148+
and self._network_stream.get_extra_info("is_readable")
149+
)
150+
if (
151+
server_disconnected
152+
or self._state == HTTPConnectionState.SERVER_DISCONNECTED
153+
):
154+
self._state = HTTPConnectionState.SERVER_DISCONNECTED
155+
raise ServerDisconnectedError()
156+
157+
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
158+
self._request_count += 1
159+
self._state = HTTPConnectionState.ACTIVE
160+
self._expire_at = None
161+
else:
162+
raise ConnectionNotAvailable()
163+
161164
# Sending the request...
162165

163166
async def _send_request_headers(self, request: Request) -> None:

httpcore/_async/http2.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,7 @@ async def handle_async_request(self, request: Request) -> Response:
9393
f"to {self._origin}"
9494
)
9595

96-
async with self._state_lock:
97-
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
98-
self._request_count += 1
99-
self._expire_at = None
100-
self._state = HTTPConnectionState.ACTIVE
101-
else:
102-
raise ConnectionNotAvailable()
96+
await self._update_state()
10397

10498
async with self._init_lock:
10599
if not self._sent_connection_init:
@@ -184,6 +178,15 @@ async def handle_async_request(self, request: Request) -> Response:
184178

185179
raise exc
186180

181+
async def _update_state(self) -> None:
182+
async with self._state_lock:
183+
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
184+
self._request_count += 1
185+
self._expire_at = None
186+
self._state = HTTPConnectionState.ACTIVE
187+
else:
188+
raise ConnectionNotAvailable()
189+
187190
async def _send_connection_init(self, request: Request) -> None:
188191
"""
189192
The HTTP/2 connection requires some initial setup before we can start

httpcore/_exceptions.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ def map_exceptions(map: ExceptionMapping) -> Iterator[None]:
1616

1717

1818
class ConnectionNotAvailable(Exception):
19-
pass
19+
"""
20+
This error is handled by the connection pool.
21+
Users should not see this error directly when using connection pool.
22+
"""
2023

2124

22-
class ServerDisconnectedError(Exception):
23-
pass
25+
class ServerDisconnectedError(ConnectionNotAvailable):
26+
"""
27+
This error is handled by the connection pool.
28+
Users should not see this error directly when using connection pool.
29+
"""
2430

2531

2632
class ProxyError(Exception):

httpcore/_sync/connection_pool.py

+17-20
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55

66
from .._backends.sync import SyncBackend
77
from .._backends.base import SOCKET_OPTION, NetworkBackend
8-
from .._exceptions import (
9-
ConnectionNotAvailable,
10-
ServerDisconnectedError,
11-
UnsupportedProtocol,
12-
)
8+
from .._exceptions import ConnectionNotAvailable, UnsupportedProtocol
139
from .._models import Origin, Request, Response
1410
from .._synchronization import Event, ShieldCancellation, ThreadLock
1511
from .connection import HTTPConnection
@@ -200,7 +196,7 @@ def handle_request(self, request: Request) -> Response:
200196
response = connection.handle_request(
201197
pool_request.request
202198
)
203-
except (ConnectionNotAvailable, ServerDisconnectedError):
199+
except ConnectionNotAvailable:
204200
# In some cases a connection may initially be available to
205201
# handle a request, but then become unavailable.
206202
#
@@ -244,7 +240,7 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]:
244240
closing_connections = []
245241

246242
# First we handle cleaning up any connections that are closed,
247-
# have expired their keep-alive, or surplus idle connections.
243+
# have expired, or surplus idle connections.
248244
for connection in list(self._connections):
249245
if connection.is_closed():
250246
# log: "removing closed connection"
@@ -271,15 +267,12 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]:
271267
for connection in self._connections
272268
if connection.can_handle_request(origin) and connection.is_available()
273269
]
274-
idle_connections = [
275-
connection for connection in self._connections if connection.is_idle()
276-
]
277270

278271
# There are three cases for how we may be able to handle the request:
279272
#
280273
# 1. There is an existing connection that can handle the request.
281274
# 2. We can create a new connection to handle the request.
282-
# 3. We can close an idle connection and then create a new connection
275+
# 3. We can close an idle/expired connection and then create a new connection
283276
# to handle the request.
284277
if available_connections:
285278
# log: "reusing existing connection"
@@ -290,15 +283,19 @@ def _assign_requests_to_connections(self) -> List[ConnectionInterface]:
290283
connection = self.create_connection(origin)
291284
self._connections.append(connection)
292285
pool_request.assign_to_connection(connection)
293-
elif idle_connections:
294-
# log: "closing idle connection"
295-
connection = idle_connections[0]
296-
self._connections.remove(connection)
297-
closing_connections.append(connection)
298-
# log: "creating new connection"
299-
connection = self.create_connection(origin)
300-
self._connections.append(connection)
301-
pool_request.assign_to_connection(connection)
286+
else:
287+
purged_connection = next(
288+
(c for c in self._connections if c.is_idle() or c.has_expired()),
289+
None,
290+
)
291+
if purged_connection is not None:
292+
# log: "closing idle connection"
293+
self._connections.remove(purged_connection)
294+
closing_connections.append(purged_connection)
295+
# log: "creating new connection"
296+
connection = self.create_connection(origin)
297+
self._connections.append(connection)
298+
pool_request.assign_to_connection(connection)
302299

303300
return closing_connections
304301

httpcore/_sync/http11.py

+24-21
Original file line numberDiff line numberDiff line change
@@ -78,27 +78,7 @@ def handle_request(self, request: Request) -> Response:
7878
f"to {self._origin}"
7979
)
8080

81-
with self._state_lock:
82-
if self._state == HTTPConnectionState.SERVER_DISCONNECTED:
83-
raise ServerDisconnectedError()
84-
85-
# If the HTTP connection is idle but the socket is readable, then the
86-
# only valid state is that the socket is about to return b"", indicating
87-
# a server-initiated disconnect.
88-
server_disconnected = (
89-
self._state == HTTPConnectionState.IDLE
90-
and self._network_stream.get_extra_info("is_readable")
91-
)
92-
if server_disconnected:
93-
self._state = HTTPConnectionState.SERVER_DISCONNECTED
94-
raise ServerDisconnectedError()
95-
96-
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
97-
self._request_count += 1
98-
self._state = HTTPConnectionState.ACTIVE
99-
self._expire_at = None
100-
else:
101-
raise ConnectionNotAvailable()
81+
self._update_state()
10282

10383
try:
10484
kwargs = {"request": request}
@@ -158,6 +138,29 @@ def handle_request(self, request: Request) -> Response:
158138
self._response_closed()
159139
raise exc
160140

141+
def _update_state(self) -> None:
142+
with self._state_lock:
143+
# If the HTTP connection is idle but the socket is readable, then the
144+
# only valid state is that the socket is about to return b"", indicating
145+
# a server-initiated disconnect.
146+
server_disconnected = (
147+
self._state == HTTPConnectionState.IDLE
148+
and self._network_stream.get_extra_info("is_readable")
149+
)
150+
if (
151+
server_disconnected
152+
or self._state == HTTPConnectionState.SERVER_DISCONNECTED
153+
):
154+
self._state = HTTPConnectionState.SERVER_DISCONNECTED
155+
raise ServerDisconnectedError()
156+
157+
if self._state in (HTTPConnectionState.NEW, HTTPConnectionState.IDLE):
158+
self._request_count += 1
159+
self._state = HTTPConnectionState.ACTIVE
160+
self._expire_at = None
161+
else:
162+
raise ConnectionNotAvailable()
163+
161164
# Sending the request...
162165

163166
def _send_request_headers(self, request: Request) -> None:

httpcore/_sync/http2.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,7 @@ def handle_request(self, request: Request) -> Response:
9393
f"to {self._origin}"
9494
)
9595

96-
with self._state_lock:
97-
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
98-
self._request_count += 1
99-
self._expire_at = None
100-
self._state = HTTPConnectionState.ACTIVE
101-
else:
102-
raise ConnectionNotAvailable()
96+
self._update_state()
10397

10498
with self._init_lock:
10599
if not self._sent_connection_init:
@@ -184,6 +178,15 @@ def handle_request(self, request: Request) -> Response:
184178

185179
raise exc
186180

181+
def _update_state(self) -> None:
182+
with self._state_lock:
183+
if self._state in (HTTPConnectionState.ACTIVE, HTTPConnectionState.IDLE):
184+
self._request_count += 1
185+
self._expire_at = None
186+
self._state = HTTPConnectionState.ACTIVE
187+
else:
188+
raise ConnectionNotAvailable()
189+
187190
def _send_connection_init(self, request: Request) -> None:
188191
"""
189192
The HTTP/2 connection requires some initial setup before we can start

tests/_async/test_http11.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pytest
44

55
import httpcore
6-
from httpcore._exceptions import ServerDisconnectedError
76

87

98
@pytest.mark.anyio
@@ -202,11 +201,7 @@ def get_extra_info(self, info: str) -> typing.Any:
202201
assert conn.is_idle() and not conn.has_expired()
203202
stream.mock_is_readable = True # Simulate connection breakage
204203

205-
with pytest.raises(ServerDisconnectedError):
206-
await conn.request("GET", "https://example.com/")
207-
assert conn.has_expired() and not conn.is_idle()
208-
209-
with pytest.raises(ServerDisconnectedError):
204+
with pytest.raises(httpcore.ServerDisconnectedError):
210205
await conn.request("GET", "https://example.com/")
211206
assert conn.has_expired() and not conn.is_idle()
212207

tests/_sync/test_http11.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import pytest
44

55
import httpcore
6-
from httpcore._exceptions import ServerDisconnectedError
76

87

98

@@ -202,11 +201,7 @@ def get_extra_info(self, info: str) -> typing.Any:
202201
assert conn.is_idle() and not conn.has_expired()
203202
stream.mock_is_readable = True # Simulate connection breakage
204203

205-
with pytest.raises(ServerDisconnectedError):
206-
conn.request("GET", "https://example.com/")
207-
assert conn.has_expired() and not conn.is_idle()
208-
209-
with pytest.raises(ServerDisconnectedError):
204+
with pytest.raises(httpcore.ServerDisconnectedError):
210205
conn.request("GET", "https://example.com/")
211206
assert conn.has_expired() and not conn.is_idle()
212207

0 commit comments

Comments
 (0)