Skip to content

Commit c353ce2

Browse files
Fix pool timeout edge-case. (#688)
1 parent bfe97bc commit c353ce2

5 files changed

Lines changed: 129 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010
- Improve correctness of tracebacks on network exceptions, by raising properly chained exceptions. (#678)
1111
- Prevent connection-hanging behaviour when HTTP/2 connections are closed by a server-sent 'GoAway' frame. (#679)
1212
- Fix edge-case exception when removing requests from the connection pool. (#680)
13+
- Fix pool timeout edge-case. (#688)
1314

1415
## 0.17.0 (March 16th, 2023)
1516

httpcore/_async/connection_pool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def unset_connection(self) -> None:
3131
async def wait_for_connection(
3232
self, timeout: Optional[float] = None
3333
) -> AsyncConnectionInterface:
34-
await self._connection_acquired.wait(timeout=timeout)
34+
if self.connection is None:
35+
await self._connection_acquired.wait(timeout=timeout)
3536
assert self.connection is not None
3637
return self.connection
3738

httpcore/_sync/connection_pool.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ def unset_connection(self) -> None:
3131
def wait_for_connection(
3232
self, timeout: Optional[float] = None
3333
) -> ConnectionInterface:
34-
self._connection_acquired.wait(timeout=timeout)
34+
if self.connection is None:
35+
self._connection_acquired.wait(timeout=timeout)
3536
assert self.connection is not None
3637
return self.connection
3738

tests/_async/test_connection_pool.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,68 @@ async def test_connection_pool_timeout():
571571
await pool.request("GET", "https://example.com/", extensions=extensions)
572572

573573

574+
@pytest.mark.anyio
575+
async def test_connection_pool_timeout_zero():
576+
"""
577+
A pool timeout of 0 shouldn't raise a PoolTimeout if there's
578+
no need to wait on a new connection.
579+
"""
580+
network_backend = AsyncMockBackend(
581+
[
582+
b"HTTP/1.1 200 OK\r\n",
583+
b"Content-Type: plain/text\r\n",
584+
b"Content-Length: 13\r\n",
585+
b"\r\n",
586+
b"Hello, world!",
587+
b"HTTP/1.1 200 OK\r\n",
588+
b"Content-Type: plain/text\r\n",
589+
b"Content-Length: 13\r\n",
590+
b"\r\n",
591+
b"Hello, world!",
592+
]
593+
)
594+
595+
# Use a pool timeout of zero.
596+
extensions = {"timeout": {"pool": 0}}
597+
598+
# A connection pool configured to allow only one connection at a time.
599+
async with AsyncConnectionPool(
600+
network_backend=network_backend, max_connections=1
601+
) as pool:
602+
# Two consecutive requests with a pool timeout of zero.
603+
# Both succeed without raising a timeout.
604+
response = await pool.request(
605+
"GET", "https://example.com/", extensions=extensions
606+
)
607+
assert response.status == 200
608+
assert response.content == b"Hello, world!"
609+
610+
response = await pool.request(
611+
"GET", "https://example.com/", extensions=extensions
612+
)
613+
assert response.status == 200
614+
assert response.content == b"Hello, world!"
615+
616+
# A connection pool configured to allow only one connection at a time.
617+
async with AsyncConnectionPool(
618+
network_backend=network_backend, max_connections=1
619+
) as pool:
620+
# Two concurrent requests with a pool timeout of zero.
621+
# Only the first will succeed without raising a timeout.
622+
async with pool.stream(
623+
"GET", "https://example.com/", extensions=extensions
624+
) as response:
625+
# The first response hasn't yet completed.
626+
with pytest.raises(PoolTimeout):
627+
# So a pool timeout occurs.
628+
await pool.request("GET", "https://example.com/", extensions=extensions)
629+
# The first response now completes.
630+
await response.aread()
631+
632+
assert response.status == 200
633+
assert response.content == b"Hello, world!"
634+
635+
574636
@pytest.mark.anyio
575637
async def test_http11_upgrade_connection():
576638
"""

tests/_sync/test_connection_pool.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,68 @@ def test_connection_pool_timeout():
572572

573573

574574

575+
def test_connection_pool_timeout_zero():
576+
"""
577+
A pool timeout of 0 shouldn't raise a PoolTimeout if there's
578+
no need to wait on a new connection.
579+
"""
580+
network_backend = MockBackend(
581+
[
582+
b"HTTP/1.1 200 OK\r\n",
583+
b"Content-Type: plain/text\r\n",
584+
b"Content-Length: 13\r\n",
585+
b"\r\n",
586+
b"Hello, world!",
587+
b"HTTP/1.1 200 OK\r\n",
588+
b"Content-Type: plain/text\r\n",
589+
b"Content-Length: 13\r\n",
590+
b"\r\n",
591+
b"Hello, world!",
592+
]
593+
)
594+
595+
# Use a pool timeout of zero.
596+
extensions = {"timeout": {"pool": 0}}
597+
598+
# A connection pool configured to allow only one connection at a time.
599+
with ConnectionPool(
600+
network_backend=network_backend, max_connections=1
601+
) as pool:
602+
# Two consecutive requests with a pool timeout of zero.
603+
# Both succeed without raising a timeout.
604+
response = pool.request(
605+
"GET", "https://example.com/", extensions=extensions
606+
)
607+
assert response.status == 200
608+
assert response.content == b"Hello, world!"
609+
610+
response = pool.request(
611+
"GET", "https://example.com/", extensions=extensions
612+
)
613+
assert response.status == 200
614+
assert response.content == b"Hello, world!"
615+
616+
# A connection pool configured to allow only one connection at a time.
617+
with ConnectionPool(
618+
network_backend=network_backend, max_connections=1
619+
) as pool:
620+
# Two concurrent requests with a pool timeout of zero.
621+
# Only the first will succeed without raising a timeout.
622+
with pool.stream(
623+
"GET", "https://example.com/", extensions=extensions
624+
) as response:
625+
# The first response hasn't yet completed.
626+
with pytest.raises(PoolTimeout):
627+
# So a pool timeout occurs.
628+
pool.request("GET", "https://example.com/", extensions=extensions)
629+
# The first response now completes.
630+
response.read()
631+
632+
assert response.status == 200
633+
assert response.content == b"Hello, world!"
634+
635+
636+
575637
def test_http11_upgrade_connection():
576638
"""
577639
HTTP "101 Switching Protocols" indicates an upgraded connection.

0 commit comments

Comments
 (0)