Skip to content

Commit f9ef93d

Browse files
author
rodrigo.nogueira
committed
Avoid redundant response release-connection calls
1 parent 92fa5ef commit f9ef93d

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

CHANGES/10089.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Reduced redundant ``ClientResponse._release_connection()`` invocations so
2+
successful requests now trigger a single release-path call per response.

aiohttp/client_reqrep.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,8 @@ async def wait_for_close(self) -> None:
583583
and task.cancelling()
584584
):
585585
raise
586-
self.release()
586+
if not self._released:
587+
self.release()
587588

588589
async def read(self) -> bytes:
589590
"""Read response payload."""

tests/test_client_functional.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,28 @@ async def handler(request: web.Request) -> web.Response:
370370
assert 1 == len(client._session.connector._conns)
371371

372372

373+
async def test_release_connection_called_three_times_per_request(
374+
aiohttp_client: AiohttpClient, mocker: MockerFixture
375+
) -> None:
376+
async def handler(request: web.Request) -> web.Response:
377+
await request.read()
378+
return web.Response(body=b"OK")
379+
380+
app = web.Application()
381+
app.router.add_route("GET", "/", handler)
382+
client = await aiohttp_client(app)
383+
384+
spy = mocker.spy(aiohttp.client_reqrep.ClientResponse, "_release_connection")
385+
for _ in range(3):
386+
async with client.get("/") as resp:
387+
await resp.read()
388+
await asyncio.sleep(0)
389+
390+
# One request with response body read in a context manager currently hits:
391+
# _response_eof(), _wait_released(), and release() from __aexit__().
392+
assert spy.call_count == 9
393+
394+
373395
async def test_HTTP_304(aiohttp_client: AiohttpClient) -> None:
374396
async def handler(request: web.Request) -> web.Response:
375397
body = await request.read()

tests/test_client_response.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,78 @@ async def test_release(loop: asyncio.AbstractEventLoop, session: ClientSession)
286286
assert response._connection is None
287287

288288

289+
async def test_wait_released_releases_connection(
290+
loop: asyncio.AbstractEventLoop, session: ClientSession
291+
) -> None:
292+
url = URL("http://def-cl-resp.org")
293+
response = ClientResponse(
294+
"get",
295+
url,
296+
writer=None,
297+
continue100=None,
298+
timer=TimerNoop(),
299+
traces=[],
300+
loop=loop,
301+
session=session,
302+
request_headers=CIMultiDict[str](),
303+
original_url=url,
304+
)
305+
conn = response._connection = mock.Mock()
306+
307+
await response._wait_released()
308+
309+
conn.release.assert_called_once_with()
310+
assert response._connection is None
311+
312+
313+
async def test_wait_released_calls_release_without_connection(
314+
loop: asyncio.AbstractEventLoop, session: ClientSession
315+
) -> None:
316+
url = URL("http://def-cl-resp.org")
317+
response = ClientResponse(
318+
"get",
319+
url,
320+
writer=None,
321+
continue100=None,
322+
timer=TimerNoop(),
323+
traces=[],
324+
loop=loop,
325+
session=session,
326+
request_headers=CIMultiDict[str](),
327+
original_url=url,
328+
)
329+
response._connection = None
330+
331+
with mock.patch.object(response, "_release_connection") as release_connection:
332+
await response._wait_released()
333+
334+
release_connection.assert_called_once_with()
335+
336+
337+
async def test_wait_for_close_skips_release_if_already_released(
338+
loop: asyncio.AbstractEventLoop, session: ClientSession
339+
) -> None:
340+
url = URL("http://def-cl-resp.org")
341+
response = ClientResponse(
342+
"get",
343+
url,
344+
writer=None,
345+
continue100=None,
346+
timer=TimerNoop(),
347+
traces=[],
348+
loop=loop,
349+
session=session,
350+
request_headers=CIMultiDict[str](),
351+
original_url=url,
352+
)
353+
response._released = True
354+
355+
with mock.patch.object(response, "release") as release:
356+
await response.wait_for_close()
357+
358+
release.assert_not_called()
359+
360+
289361
@pytest.mark.skipif(
290362
sys.implementation.name != "cpython",
291363
reason="Other implementations has different GC strategies",

0 commit comments

Comments
 (0)