Skip to content

Commit 0a29170

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

File tree

4 files changed

+100
-3
lines changed

4 files changed

+100
-3
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: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,8 @@ def release(self) -> None:
510510
self._closed = True
511511

512512
self._cleanup_writer()
513-
self._release_connection()
513+
if self._connection is not None:
514+
self._release_connection()
514515

515516
@property
516517
def ok(self) -> bool:
@@ -558,7 +559,8 @@ async def _wait_released(self) -> None:
558559
and task.cancelling()
559560
):
560561
raise
561-
self._release_connection()
562+
if self._connection is not None:
563+
self._release_connection()
562564

563565
def _cleanup_writer(self) -> None:
564566
if self.__writer is not None:
@@ -583,7 +585,8 @@ async def wait_for_close(self) -> None:
583585
and task.cancelling()
584586
):
585587
raise
586-
self.release()
588+
if not self._released:
589+
self.release()
587590

588591
async def read(self) -> bytes:
589592
"""Read response payload."""

tests/test_client_functional.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,26 @@ 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_once_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+
assert spy.call_count == 3
391+
392+
373393
async def test_HTTP_304(aiohttp_client: AiohttpClient) -> None:
374394
async def handler(request: web.Request) -> web.Response:
375395
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_skips_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_not_called()
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)