Skip to content

Commit a406468

Browse files
igor.stulikovigor.stulikov
igor.stulikov
authored and
igor.stulikov
committed
Fix pool poisoning on overloaded systems (encode#550)
1 parent 7eb2022 commit a406468

24 files changed

+112
-0
lines changed

httpcore/_async/connection.py

+3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ def is_closed(self) -> bool:
185185
return self._connect_failed
186186
return self._connection.is_closed()
187187

188+
def is_connecting(self) -> bool:
189+
return self._connection is None and not self._connect_failed
190+
188191
def info(self) -> str:
189192
if self._connection is None:
190193
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"

httpcore/_async/connection_pool.py

+19
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def connections(self) -> List[AsyncConnectionInterface]:
140140
"""
141141
return list(self._pool)
142142

143+
@property
144+
def _is_pool_full(self) -> bool:
145+
return len(self._pool) >= self._max_connections
146+
143147
async def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool:
144148
"""
145149
Attempt to provide a connection that can handle the given origin.
@@ -168,6 +172,21 @@ async def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool:
168172
self._pool.pop(idx)
169173
break
170174

175+
# Attempt to close CONNECTING connections that no one needs
176+
if self._is_pool_full:
177+
for idx, connection in enumerate(self._pool): # Try to check old connections first
178+
if not connection.is_connecting():
179+
continue
180+
for req_status in self._requests:
181+
if req_status is status: # skip current request
182+
continue
183+
if connection.can_handle_request(req_status.request.url.origin):
184+
break
185+
else: # There is no requests that can be handled by this connection
186+
await connection.aclose()
187+
self._pool.pop(idx)
188+
break
189+
171190
# If the pool is still full, then we cannot acquire a connection.
172191
if len(self._pool) >= self._max_connections:
173192
return False

httpcore/_async/http11.py

+3
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ def is_idle(self) -> bool:
269269
def is_closed(self) -> bool:
270270
return self._state == HTTPConnectionState.CLOSED
271271

272+
def is_connecting(self) -> bool:
273+
return self._state == HTTPConnectionState.NEW
274+
272275
def info(self) -> str:
273276
origin = str(self._origin)
274277
return (

httpcore/_async/http2.py

+3
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ def is_idle(self) -> bool:
411411
def is_closed(self) -> bool:
412412
return self._state == HTTPConnectionState.CLOSED
413413

414+
def is_connecting(self) -> bool:
415+
return False
416+
414417
def info(self) -> str:
415418
origin = str(self._origin)
416419
return (

httpcore/_async/http_proxy.py

+6
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ def is_idle(self) -> bool:
204204
def is_closed(self) -> bool:
205205
return self._connection.is_closed()
206206

207+
def is_connecting(self) -> bool:
208+
return self._connection.is_connecting()
209+
207210
def __repr__(self) -> str:
208211
return f"<{self.__class__.__name__} [{self.info()}]>"
209212

@@ -336,5 +339,8 @@ def is_idle(self) -> bool:
336339
def is_closed(self) -> bool:
337340
return self._connection.is_closed()
338341

342+
def is_connecting(self) -> bool:
343+
return self._connection.is_connecting()
344+
339345
def __repr__(self) -> str:
340346
return f"<{self.__class__.__name__} [{self.info()}]>"

httpcore/_async/interfaces.py

+6
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,9 @@ def is_closed(self) -> bool:
133133
returned to the connection pool or not.
134134
"""
135135
raise NotImplementedError() # pragma: nocover
136+
137+
def is_connecting(self) -> bool:
138+
"""
139+
Return `True` if the connection is currently connecting. For HTTP/2 connection always returns `False`
140+
"""
141+
raise NotImplementedError() # pragma: nocover

httpcore/_async/socks_proxy.py

+3
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ def is_closed(self) -> bool:
329329
return self._connect_failed
330330
return self._connection.is_closed()
331331

332+
def is_connecting(self) -> bool:
333+
return self._connection is None and not self._connect_failed
334+
332335
def info(self) -> str:
333336
if self._connection is None: # pragma: nocover
334337
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"

httpcore/_sync/connection.py

+3
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,9 @@ def is_closed(self) -> bool:
185185
return self._connect_failed
186186
return self._connection.is_closed()
187187

188+
def is_connecting(self) -> bool:
189+
return self._connection is None and not self._connect_failed
190+
188191
def info(self) -> str:
189192
if self._connection is None:
190193
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"

httpcore/_sync/connection_pool.py

+19
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ def connections(self) -> List[ConnectionInterface]:
140140
"""
141141
return list(self._pool)
142142

143+
@property
144+
def _is_pool_full(self) -> bool:
145+
return len(self._pool) >= self._max_connections
146+
143147
def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool:
144148
"""
145149
Attempt to provide a connection that can handle the given origin.
@@ -168,6 +172,21 @@ def _attempt_to_acquire_connection(self, status: RequestStatus) -> bool:
168172
self._pool.pop(idx)
169173
break
170174

175+
# Attempt to close CONNECTING connections that no one needs
176+
if self._is_pool_full:
177+
for idx, connection in enumerate(self._pool): # Try to check old connections first
178+
if not connection.is_connecting():
179+
continue
180+
for req_status in self._requests:
181+
if req_status is status: # skip current request
182+
continue
183+
if connection.can_handle_request(req_status.request.url.origin):
184+
break
185+
else: # There is no requests that can be handled by this connection
186+
connection.close()
187+
self._pool.pop(idx)
188+
break
189+
171190
# If the pool is still full, then we cannot acquire a connection.
172191
if len(self._pool) >= self._max_connections:
173192
return False

httpcore/_sync/http11.py

+3
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,9 @@ def is_idle(self) -> bool:
269269
def is_closed(self) -> bool:
270270
return self._state == HTTPConnectionState.CLOSED
271271

272+
def is_connecting(self) -> bool:
273+
return self._state == HTTPConnectionState.NEW
274+
272275
def info(self) -> str:
273276
origin = str(self._origin)
274277
return (

httpcore/_sync/http2.py

+3
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,9 @@ def is_idle(self) -> bool:
411411
def is_closed(self) -> bool:
412412
return self._state == HTTPConnectionState.CLOSED
413413

414+
def is_connecting(self) -> bool:
415+
return False
416+
414417
def info(self) -> str:
415418
origin = str(self._origin)
416419
return (

httpcore/_sync/http_proxy.py

+6
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,9 @@ def is_idle(self) -> bool:
204204
def is_closed(self) -> bool:
205205
return self._connection.is_closed()
206206

207+
def is_connecting(self) -> bool:
208+
return self._connection.is_connecting()
209+
207210
def __repr__(self) -> str:
208211
return f"<{self.__class__.__name__} [{self.info()}]>"
209212

@@ -336,5 +339,8 @@ def is_idle(self) -> bool:
336339
def is_closed(self) -> bool:
337340
return self._connection.is_closed()
338341

342+
def is_connecting(self) -> bool:
343+
return self._connection.is_connecting()
344+
339345
def __repr__(self) -> str:
340346
return f"<{self.__class__.__name__} [{self.info()}]>"

httpcore/_sync/interfaces.py

+6
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,9 @@ def is_closed(self) -> bool:
133133
returned to the connection pool or not.
134134
"""
135135
raise NotImplementedError() # pragma: nocover
136+
137+
def is_connecting(self) -> bool:
138+
"""
139+
Return `True` if the connection is currently connecting. For HTTP/2 connection always returns `False`
140+
"""
141+
raise NotImplementedError() # pragma: nocover

httpcore/_sync/socks_proxy.py

+3
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ def is_closed(self) -> bool:
329329
return self._connect_failed
330330
return self._connection.is_closed()
331331

332+
def is_connecting(self) -> bool:
333+
return self._connection is None and not self._connect_failed
334+
332335
def info(self) -> str:
333336
if self._connection is None: # pragma: nocover
334337
return "CONNECTION FAILED" if self._connect_failed else "CONNECTING"

tests/_async/test_connection.py

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async def test_http_connection():
2929
assert not conn.is_closed()
3030
assert not conn.is_available()
3131
assert not conn.has_expired()
32+
assert conn.is_connecting()
3233
assert repr(conn) == "<AsyncHTTPConnection [CONNECTING]>"
3334

3435
async with conn.stream("GET", "https://example.com/") as response:
@@ -45,6 +46,7 @@ async def test_http_connection():
4546
assert not conn.is_closed()
4647
assert conn.is_available()
4748
assert not conn.has_expired()
49+
assert not conn.is_connecting()
4850
assert (
4951
repr(conn)
5052
== "<AsyncHTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 1]>"

tests/_async/test_http11.py

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ async def test_http11_connection():
3333
assert not conn.is_closed()
3434
assert conn.is_available()
3535
assert not conn.has_expired()
36+
assert not conn.is_connecting()
3637
assert (
3738
repr(conn)
3839
== "<AsyncHTTP11Connection ['https://example.com:443', IDLE, Request Count: 1]>"
@@ -63,6 +64,7 @@ async def test_http11_connection_unread_response():
6364
assert conn.is_closed()
6465
assert not conn.is_available()
6566
assert not conn.has_expired()
67+
assert not conn.is_connecting()
6668
assert (
6769
repr(conn)
6870
== "<AsyncHTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -85,6 +87,7 @@ async def test_http11_connection_with_remote_protocol_error():
8587
assert conn.is_closed()
8688
assert not conn.is_available()
8789
assert not conn.has_expired()
90+
assert not conn.is_connecting()
8891
assert (
8992
repr(conn)
9093
== "<AsyncHTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -114,6 +117,7 @@ async def test_http11_connection_with_incomplete_response():
114117
assert conn.is_closed()
115118
assert not conn.is_available()
116119
assert not conn.has_expired()
120+
assert not conn.is_connecting()
117121
assert (
118122
repr(conn)
119123
== "<AsyncHTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -146,6 +150,7 @@ async def test_http11_connection_with_local_protocol_error():
146150
assert conn.is_closed()
147151
assert not conn.is_available()
148152
assert not conn.has_expired()
153+
assert not conn.is_connecting()
149154
assert (
150155
repr(conn)
151156
== "<AsyncHTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"

tests/_async/test_http2.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ async def test_http2_connection():
4343
assert conn.is_available()
4444
assert not conn.is_closed()
4545
assert not conn.has_expired()
46+
assert not conn.is_connecting()
4647
assert (
4748
conn.info() == "'https://example.com:443', HTTP/2, IDLE, Request Count: 1"
4849
)

tests/_async/test_http_proxy.py

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ async def test_proxy_forwarding():
4747
assert proxy.connections[0].is_idle()
4848
assert proxy.connections[0].is_available()
4949
assert not proxy.connections[0].is_closed()
50+
assert not proxy.connections[0].is_connecting()
5051

5152
# A connection on a forwarding proxy can only handle HTTP requests to the same origin.
5253
assert proxy.connections[0].can_handle_request(
@@ -102,6 +103,7 @@ async def test_proxy_tunneling():
102103
assert proxy.connections[0].is_idle()
103104
assert proxy.connections[0].is_available()
104105
assert not proxy.connections[0].is_closed()
106+
assert not proxy.connections[0].is_connecting()
105107

106108
# A connection on a tunneled proxy can only handle HTTPS requests to the same origin.
107109
assert not proxy.connections[0].can_handle_request(
@@ -193,6 +195,7 @@ async def test_proxy_tunneling_http2():
193195
assert proxy.connections[0].is_idle()
194196
assert proxy.connections[0].is_available()
195197
assert not proxy.connections[0].is_closed()
198+
assert not proxy.connections[0].is_connecting()
196199

197200
# A connection on a tunneled proxy can only handle HTTPS requests to the same origin.
198201
assert not proxy.connections[0].can_handle_request(

tests/_async/test_socks_proxy.py

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async def test_socks5_request():
4646
assert proxy.connections[0].is_idle()
4747
assert proxy.connections[0].is_available()
4848
assert not proxy.connections[0].is_closed()
49+
assert not proxy.connections[0].is_connecting()
4950

5051
# A connection on a tunneled proxy can only handle HTTPS requests to the same origin.
5152
assert not proxy.connections[0].can_handle_request(
@@ -107,6 +108,7 @@ async def test_authenticated_socks5_request():
107108
assert proxy.connections[0].is_idle()
108109
assert proxy.connections[0].is_available()
109110
assert not proxy.connections[0].is_closed()
111+
assert not proxy.connections[0].is_connecting()
110112

111113

112114
@pytest.mark.anyio

tests/_sync/test_connection.py

+2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_http_connection():
2929
assert not conn.is_closed()
3030
assert not conn.is_available()
3131
assert not conn.has_expired()
32+
assert conn.is_connecting()
3233
assert repr(conn) == "<HTTPConnection [CONNECTING]>"
3334

3435
with conn.stream("GET", "https://example.com/") as response:
@@ -45,6 +46,7 @@ def test_http_connection():
4546
assert not conn.is_closed()
4647
assert conn.is_available()
4748
assert not conn.has_expired()
49+
assert not conn.is_connecting()
4850
assert (
4951
repr(conn)
5052
== "<HTTPConnection ['https://example.com:443', HTTP/1.1, IDLE, Request Count: 1]>"

tests/_sync/test_http11.py

+5
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_http11_connection():
3333
assert not conn.is_closed()
3434
assert conn.is_available()
3535
assert not conn.has_expired()
36+
assert not conn.is_connecting()
3637
assert (
3738
repr(conn)
3839
== "<HTTP11Connection ['https://example.com:443', IDLE, Request Count: 1]>"
@@ -63,6 +64,7 @@ def test_http11_connection_unread_response():
6364
assert conn.is_closed()
6465
assert not conn.is_available()
6566
assert not conn.has_expired()
67+
assert not conn.is_connecting()
6668
assert (
6769
repr(conn)
6870
== "<HTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -85,6 +87,7 @@ def test_http11_connection_with_remote_protocol_error():
8587
assert conn.is_closed()
8688
assert not conn.is_available()
8789
assert not conn.has_expired()
90+
assert not conn.is_connecting()
8891
assert (
8992
repr(conn)
9093
== "<HTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -114,6 +117,7 @@ def test_http11_connection_with_incomplete_response():
114117
assert conn.is_closed()
115118
assert not conn.is_available()
116119
assert not conn.has_expired()
120+
assert not conn.is_connecting()
117121
assert (
118122
repr(conn)
119123
== "<HTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"
@@ -146,6 +150,7 @@ def test_http11_connection_with_local_protocol_error():
146150
assert conn.is_closed()
147151
assert not conn.is_available()
148152
assert not conn.has_expired()
153+
assert not conn.is_connecting()
149154
assert (
150155
repr(conn)
151156
== "<HTTP11Connection ['https://example.com:443', CLOSED, Request Count: 1]>"

tests/_sync/test_http2.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def test_http2_connection():
4343
assert conn.is_available()
4444
assert not conn.is_closed()
4545
assert not conn.has_expired()
46+
assert not conn.is_connecting()
4647
assert (
4748
conn.info() == "'https://example.com:443', HTTP/2, IDLE, Request Count: 1"
4849
)

tests/_sync/test_http_proxy.py

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def test_proxy_forwarding():
4747
assert proxy.connections[0].is_idle()
4848
assert proxy.connections[0].is_available()
4949
assert not proxy.connections[0].is_closed()
50+
assert not proxy.connections[0].is_connecting()
5051

5152
# A connection on a forwarding proxy can only handle HTTP requests to the same origin.
5253
assert proxy.connections[0].can_handle_request(
@@ -102,6 +103,7 @@ def test_proxy_tunneling():
102103
assert proxy.connections[0].is_idle()
103104
assert proxy.connections[0].is_available()
104105
assert not proxy.connections[0].is_closed()
106+
assert not proxy.connections[0].is_connecting()
105107

106108
# A connection on a tunneled proxy can only handle HTTPS requests to the same origin.
107109
assert not proxy.connections[0].can_handle_request(
@@ -193,6 +195,7 @@ def test_proxy_tunneling_http2():
193195
assert proxy.connections[0].is_idle()
194196
assert proxy.connections[0].is_available()
195197
assert not proxy.connections[0].is_closed()
198+
assert not proxy.connections[0].is_connecting()
196199

197200
# A connection on a tunneled proxy can only handle HTTPS requests to the same origin.
198201
assert not proxy.connections[0].can_handle_request(

0 commit comments

Comments
 (0)