Skip to content
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

Fix a problem with connection waiters that are never awaited #4562

Merged
merged 5 commits into from
Oct 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/4562.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a problem with connection waiters that are never awaited.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ Igor Alexandrov
Igor Davydenko
Igor Mozharovsky
Igor Pavlov
Illia Volochii
Ilya Chichak
Ilya Gruzinov
Ingmar Steen
Expand Down
24 changes: 10 additions & 14 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,8 +453,7 @@ async def connect(self, req: 'ClientRequest',
fut = self._loop.create_future()

# This connection will now count towards the limit.
waiters = self._waiters[key]
waiters.append(fut)
self._waiters[key].append(fut)

if traces:
for trace in traces:
Expand All @@ -463,21 +462,18 @@ async def connect(self, req: 'ClientRequest',
try:
await fut
except BaseException as e:
# remove a waiter even if it was cancelled, normally it's
# removed when it's notified
try:
waiters.remove(fut)
except ValueError: # fut may no longer be in list
pass
if key in self._waiters:
# remove a waiter even if it was cancelled, normally it's
# removed when it's notified
try:
self._waiters[key].remove(fut)
except ValueError: # fut may no longer be in list
pass

raise e
finally:
if not waiters:
try:
del self._waiters[key]
except KeyError:
# the key was evicted before.
pass
if key in self._waiters and not self._waiters[key]:
del self._waiters[key]

if traces:
for trace in traces:
Expand Down
39 changes: 39 additions & 0 deletions tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -2367,3 +2367,42 @@ async def send_dns_cache_hit(self, *args, **kwargs):
connector._throttle_dns_events[key] = EventResultOrError(loop)
traces = [DummyTracer()]
assert await connector._resolve_host("", 0, traces) == [token]


async def test_connector_does_not_remove_needed_waiters(loop, key) -> None:
proto = create_mocked_conn(loop)
proto.is_connected.return_value = True

req = ClientRequest('GET', URL('https://localhost:80'), loop=loop)
connection_key = req.connection_key

connector = aiohttp.BaseConnector()
connector._available_connections = mock.Mock(return_value=0)
connector._conns[key] = [(proto, loop.time())]
connector._create_connection = create_mocked_conn(loop)
connector._create_connection.return_value = loop.create_future()
connector._create_connection.return_value.set_result(proto)

dummy_waiter = loop.create_future()

async def await_connection_and_check_waiters():
connection = await connector.connect(req, [], ClientTimeout())
try:
assert connection_key in connector._waiters
assert dummy_waiter in connector._waiters[connection_key]
finally:
connection.close()

async def allow_connection_and_add_dummy_waiter():
# `asyncio.gather` may execute coroutines not in order.
# Skip one event loop run cycle in such a case.
if connection_key not in connector._waiters:
await asyncio.sleep(0)
connector._waiters[connection_key].popleft().set_result(None)
del connector._waiters[connection_key]
connector._waiters[connection_key].append(dummy_waiter)

await asyncio.gather(
await_connection_and_check_waiters(),
allow_connection_and_add_dummy_waiter(),
)