From ab8786a76c6b0a3df19d6cc2eea505433e24065e Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 15:06:32 +0100 Subject: [PATCH 01/12] gh-86802: Fix asyncio memory leak; shielded tasks where cancelled log once through the exception handler --- Lib/asyncio/tasks.py | 30 ++++++++++++++++++++++------- Lib/test/test_asyncio/test_tasks.py | 28 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 888615f8e5e1b3..39a5630d65176f 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -908,6 +908,19 @@ def _done_callback(fut, cur_task=cur_task): return outer +def _log_on_cancel_callback(inner): + if not inner.cancelled(): + exc = inner.exception() + context = { + 'message': + f'{exc.__class__.__name__} exception in shielded future', + 'exception': exc, + 'future': inner, + } + if inner._source_traceback: + context['source_traceback'] = inner._source_traceback + inner._loop.call_exception_handler(context) + def shield(arg): """Wait for a future, shielding it from cancellation. @@ -953,14 +966,11 @@ def shield(arg): else: cur_task = None - def _inner_done_callback(inner, cur_task=cur_task): - if cur_task is not None: - futures.future_discard_from_awaited_by(inner, cur_task) + def _clear_awaited_by_callback(inner): + futures.future_discard_from_awaited_by(inner, cur_task) + def _inner_done_callback(inner): if outer.cancelled(): - if not inner.cancelled(): - # Mark inner's result as retrieved. - inner.exception() return if inner.cancelled(): @@ -972,10 +982,16 @@ def _inner_done_callback(inner, cur_task=cur_task): else: outer.set_result(inner.result()) - def _outer_done_callback(outer): if not inner.done(): inner.remove_done_callback(_inner_done_callback) + # Keep only one callback to log on cancel + inner.remove_done_callback(_log_on_cancel_callback) + inner.add_done_callback(_log_on_cancel_callback) + + if cur_task is not None: + inner.add_done_callback(_clear_awaited_by_callback) + inner.add_done_callback(_inner_done_callback) outer.add_done_callback(_outer_done_callback) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 44498ef790e450..2c234d75813129 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2116,6 +2116,34 @@ def test_shield_cancel_outer(self): self.assertTrue(outer.cancelled()) self.assertEqual(0, 0 if outer._callbacks is None else len(outer._callbacks)) + def test_shield_cancel_outer_exception(self): + mock_handler = mock.Mock() + self.loop.set_exception_handler(mock_handler) + inner = self.new_future(self.loop) + outer = asyncio.shield(inner) + test_utils.run_briefly(self.loop) + outer.cancel() + test_utils.run_briefly(self.loop) + inner.set_exception(Exception('foo')) + test_utils.run_briefly(self.loop) + mock_handler.assert_called_once() + + def test_shield_duplicate_log_once(self): + mock_handler = mock.Mock() + self.loop.set_exception_handler(mock_handler) + inner = self.new_future(self.loop) + outer = asyncio.shield(inner) + test_utils.run_briefly(self.loop) + outer.cancel() + test_utils.run_briefly(self.loop) + outer = asyncio.shield(inner) + test_utils.run_briefly(self.loop) + outer.cancel() + test_utils.run_briefly(self.loop) + inner.set_exception(Exception('foo')) + test_utils.run_briefly(self.loop) + mock_handler.assert_called_once() + def test_shield_shortcut(self): fut = self.new_future(self.loop) fut.set_result(42) From b500a1f86f1ef1b8bb382e7fe4f6666545af7f55 Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 15:14:00 +0100 Subject: [PATCH 02/12] Add blurb --- .../next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst b/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst new file mode 100644 index 00000000000000..d3117b16f04436 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst @@ -0,0 +1,3 @@ +Fixed asyncio memory leak in cancelled shield tasks. For shielded tasks +where the shield was cancelled, log potential exceptions through the +exception handler. Contributed by Christian Harries. From 37837edade2aa9d664a54e3378b06d010e938545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 20 May 2025 16:15:03 +0200 Subject: [PATCH 03/12] Fix formatting --- Lib/asyncio/tasks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 39a5630d65176f..dbe5e5f26d154d 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -921,6 +921,7 @@ def _log_on_cancel_callback(inner): context['source_traceback'] = inner._source_traceback inner._loop.call_exception_handler(context) + def shield(arg): """Wait for a future, shielding it from cancellation. From 0d595af36930374e21e534185059a18c3efc637a Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 15:30:20 +0100 Subject: [PATCH 04/12] fix handle case with no exception --- Lib/asyncio/tasks.py | 33 +++++++++++++++++------------ Lib/test/test_asyncio/test_tasks.py | 12 +++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index dbe5e5f26d154d..fbd5c39a7c56ac 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -908,18 +908,23 @@ def _done_callback(fut, cur_task=cur_task): return outer -def _log_on_cancel_callback(inner): - if not inner.cancelled(): - exc = inner.exception() - context = { - 'message': - f'{exc.__class__.__name__} exception in shielded future', - 'exception': exc, - 'future': inner, - } - if inner._source_traceback: - context['source_traceback'] = inner._source_traceback - inner._loop.call_exception_handler(context) +def _log_on_exception(fut): + if fut.cancelled(): + return + + exc = fut.exception() + if exc is None: + return + + context = { + 'message': + f'{exc.__class__.__name__} exception in shielded future', + 'exception': exc, + 'future': fut, + } + if fut._source_traceback: + context['source_traceback'] = fut._source_traceback + fut._loop.call_exception_handler(context) def shield(arg): @@ -987,8 +992,8 @@ def _outer_done_callback(outer): if not inner.done(): inner.remove_done_callback(_inner_done_callback) # Keep only one callback to log on cancel - inner.remove_done_callback(_log_on_cancel_callback) - inner.add_done_callback(_log_on_cancel_callback) + inner.remove_done_callback(_log_on_exception) + inner.add_done_callback(_log_on_exception) if cur_task is not None: inner.add_done_callback(_clear_awaited_by_callback) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 2c234d75813129..f6f976f213ac02 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2116,6 +2116,18 @@ def test_shield_cancel_outer(self): self.assertTrue(outer.cancelled()) self.assertEqual(0, 0 if outer._callbacks is None else len(outer._callbacks)) + def test_shield_cancel_outer_result(self): + mock_handler = mock.Mock() + self.loop.set_exception_handler(mock_handler) + inner = self.new_future(self.loop) + outer = asyncio.shield(inner) + test_utils.run_briefly(self.loop) + outer.cancel() + test_utils.run_briefly(self.loop) + inner.set_result(1) + test_utils.run_briefly(self.loop) + mock_handler.assert_not_called() + def test_shield_cancel_outer_exception(self): mock_handler = mock.Mock() self.loop.set_exception_handler(mock_handler) From 17d39e9a5f5a23e9df5eb58e4fff1a9eb55e955a Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:23:04 +0100 Subject: [PATCH 05/12] Fix offset with connection backlog - adapted the tests - created new tests for 0 and 1 connections --- Lib/asyncio/selector_events.py | 2 +- Lib/test/test_asyncio/test_selector_events.py | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 22147451fa7ebd..6ad84044adf146 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -173,7 +173,7 @@ def _accept_connection( # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. # See https://bugs.python.org/issue27906 for more details. - for _ in range(backlog): + for _ in range(backlog + 1): try: conn, addr = sock.accept() if self._debug: diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index de81936b7456f2..d03e2fb0c60e33 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -347,6 +347,19 @@ def test_process_events_write_cancelled(self): selectors.EVENT_WRITE)]) self.loop._remove_writer.assert_called_with(1) + def test_accept_connection_zero_one(self): + for backlog in [0, 1]: + sock = mock.Mock() + sock.accept.return_value = (mock.Mock(), mock.Mock()) + with self.subTest(backlog): + mock_obj = mock.patch.object + with mock_obj(self.loop, '_accept_connection2') as accept2_mock: + self.loop._accept_connection( + mock.Mock(), sock, backlog=backlog) + self.loop.run_until_complete(asyncio.sleep(0)) + print("Accepted vs Backlog:", sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) + def test_accept_connection_multiple(self): sock = mock.Mock() sock.accept.return_value = (mock.Mock(), mock.Mock()) @@ -362,7 +375,7 @@ def test_accept_connection_multiple(self): self.loop._accept_connection( mock.Mock(), sock, backlog=backlog) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) def test_accept_connection_skip_connectionabortederror(self): sock = mock.Mock() @@ -388,7 +401,7 @@ def mock_sock_accept(): # as in test_accept_connection_multiple avoid task pending # warnings by using asyncio.sleep(0) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) class SelectorTransportTests(test_utils.TestCase): From d386169bfcddfdab230d1c1961aa4ea452360f2e Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:26:05 +0100 Subject: [PATCH 06/12] Revert "Fix offset with connection backlog - adapted the tests - created new tests for 0 and 1 connections" This reverts commit 17d39e9a5f5a23e9df5eb58e4fff1a9eb55e955a. --- Lib/asyncio/selector_events.py | 2 +- Lib/test/test_asyncio/test_selector_events.py | 17 ++--------------- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 6ad84044adf146..22147451fa7ebd 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -173,7 +173,7 @@ def _accept_connection( # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. # See https://bugs.python.org/issue27906 for more details. - for _ in range(backlog + 1): + for _ in range(backlog): try: conn, addr = sock.accept() if self._debug: diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index d03e2fb0c60e33..de81936b7456f2 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -347,19 +347,6 @@ def test_process_events_write_cancelled(self): selectors.EVENT_WRITE)]) self.loop._remove_writer.assert_called_with(1) - def test_accept_connection_zero_one(self): - for backlog in [0, 1]: - sock = mock.Mock() - sock.accept.return_value = (mock.Mock(), mock.Mock()) - with self.subTest(backlog): - mock_obj = mock.patch.object - with mock_obj(self.loop, '_accept_connection2') as accept2_mock: - self.loop._accept_connection( - mock.Mock(), sock, backlog=backlog) - self.loop.run_until_complete(asyncio.sleep(0)) - print("Accepted vs Backlog:", sock.accept.call_count, backlog) - self.assertEqual(sock.accept.call_count, backlog + 1) - def test_accept_connection_multiple(self): sock = mock.Mock() sock.accept.return_value = (mock.Mock(), mock.Mock()) @@ -375,7 +362,7 @@ def test_accept_connection_multiple(self): self.loop._accept_connection( mock.Mock(), sock, backlog=backlog) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog + 1) + self.assertEqual(sock.accept.call_count, backlog) def test_accept_connection_skip_connectionabortederror(self): sock = mock.Mock() @@ -401,7 +388,7 @@ def mock_sock_accept(): # as in test_accept_connection_multiple avoid task pending # warnings by using asyncio.sleep(0) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog + 1) + self.assertEqual(sock.accept.call_count, backlog) class SelectorTransportTests(test_utils.TestCase): From 2c603b9c3640efdd2e6c90c786ed8334c7f2fc3e Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:28:56 +0100 Subject: [PATCH 07/12] fix connection backlog offset - updated tests - added tests for 0 and 1 --- Lib/asyncio/selector_events.py | 2 +- Lib/test/test_asyncio/test_selector_events.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 22147451fa7ebd..6ad84044adf146 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -173,7 +173,7 @@ def _accept_connection( # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. # See https://bugs.python.org/issue27906 for more details. - for _ in range(backlog): + for _ in range(backlog + 1): try: conn, addr = sock.accept() if self._debug: diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index de81936b7456f2..aab6a779170eb9 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -347,6 +347,18 @@ def test_process_events_write_cancelled(self): selectors.EVENT_WRITE)]) self.loop._remove_writer.assert_called_with(1) + def test_accept_connection_zero_one(self): + for backlog in [0, 1]: + sock = mock.Mock() + sock.accept.return_value = (mock.Mock(), mock.Mock()) + with self.subTest(backlog): + mock_obj = mock.patch.object + with mock_obj(self.loop, '_accept_connection2') as accept2_mock: + self.loop._accept_connection( + mock.Mock(), sock, backlog=backlog) + self.loop.run_until_complete(asyncio.sleep(0)) + self.assertEqual(sock.accept.call_count, backlog + 1) + def test_accept_connection_multiple(self): sock = mock.Mock() sock.accept.return_value = (mock.Mock(), mock.Mock()) @@ -362,7 +374,7 @@ def test_accept_connection_multiple(self): self.loop._accept_connection( mock.Mock(), sock, backlog=backlog) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) def test_accept_connection_skip_connectionabortederror(self): sock = mock.Mock() @@ -388,7 +400,7 @@ def mock_sock_accept(): # as in test_accept_connection_multiple avoid task pending # warnings by using asyncio.sleep(0) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) class SelectorTransportTests(test_utils.TestCase): From 39f4654b1cec2d317d6c019ec89cf69fffd8e0b0 Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:30:30 +0100 Subject: [PATCH 08/12] Revert "fix connection backlog offset - updated tests - added tests for 0 and 1" This reverts commit 2c603b9c3640efdd2e6c90c786ed8334c7f2fc3e. --- Lib/asyncio/selector_events.py | 2 +- Lib/test/test_asyncio/test_selector_events.py | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 6ad84044adf146..22147451fa7ebd 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -173,7 +173,7 @@ def _accept_connection( # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. # See https://bugs.python.org/issue27906 for more details. - for _ in range(backlog + 1): + for _ in range(backlog): try: conn, addr = sock.accept() if self._debug: diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index aab6a779170eb9..de81936b7456f2 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -347,18 +347,6 @@ def test_process_events_write_cancelled(self): selectors.EVENT_WRITE)]) self.loop._remove_writer.assert_called_with(1) - def test_accept_connection_zero_one(self): - for backlog in [0, 1]: - sock = mock.Mock() - sock.accept.return_value = (mock.Mock(), mock.Mock()) - with self.subTest(backlog): - mock_obj = mock.patch.object - with mock_obj(self.loop, '_accept_connection2') as accept2_mock: - self.loop._accept_connection( - mock.Mock(), sock, backlog=backlog) - self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog + 1) - def test_accept_connection_multiple(self): sock = mock.Mock() sock.accept.return_value = (mock.Mock(), mock.Mock()) @@ -374,7 +362,7 @@ def test_accept_connection_multiple(self): self.loop._accept_connection( mock.Mock(), sock, backlog=backlog) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog + 1) + self.assertEqual(sock.accept.call_count, backlog) def test_accept_connection_skip_connectionabortederror(self): sock = mock.Mock() @@ -400,7 +388,7 @@ def mock_sock_accept(): # as in test_accept_connection_multiple avoid task pending # warnings by using asyncio.sleep(0) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog + 1) + self.assertEqual(sock.accept.call_count, backlog) class SelectorTransportTests(test_utils.TestCase): From 5893d43d8482777bf9c4adbe0f67c410cc0891ef Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:31:28 +0100 Subject: [PATCH 09/12] Revert "gh-86802: Fix asyncio memory leak; shielded task exceptions log once through the exception handler (gh-134331)" This reverts commit f695eca60cfc53cf3322323082652037d6d0cfef. --- Lib/asyncio/tasks.py | 36 ++++------------- Lib/test/test_asyncio/test_tasks.py | 40 ------------------- ...5-05-20-15-13-43.gh-issue-86802.trF7TM.rst | 3 -- 3 files changed, 7 insertions(+), 72 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index fbd5c39a7c56ac..888615f8e5e1b3 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -908,25 +908,6 @@ def _done_callback(fut, cur_task=cur_task): return outer -def _log_on_exception(fut): - if fut.cancelled(): - return - - exc = fut.exception() - if exc is None: - return - - context = { - 'message': - f'{exc.__class__.__name__} exception in shielded future', - 'exception': exc, - 'future': fut, - } - if fut._source_traceback: - context['source_traceback'] = fut._source_traceback - fut._loop.call_exception_handler(context) - - def shield(arg): """Wait for a future, shielding it from cancellation. @@ -972,11 +953,14 @@ def shield(arg): else: cur_task = None - def _clear_awaited_by_callback(inner): - futures.future_discard_from_awaited_by(inner, cur_task) + def _inner_done_callback(inner, cur_task=cur_task): + if cur_task is not None: + futures.future_discard_from_awaited_by(inner, cur_task) - def _inner_done_callback(inner): if outer.cancelled(): + if not inner.cancelled(): + # Mark inner's result as retrieved. + inner.exception() return if inner.cancelled(): @@ -988,16 +972,10 @@ def _inner_done_callback(inner): else: outer.set_result(inner.result()) + def _outer_done_callback(outer): if not inner.done(): inner.remove_done_callback(_inner_done_callback) - # Keep only one callback to log on cancel - inner.remove_done_callback(_log_on_exception) - inner.add_done_callback(_log_on_exception) - - if cur_task is not None: - inner.add_done_callback(_clear_awaited_by_callback) - inner.add_done_callback(_inner_done_callback) outer.add_done_callback(_outer_done_callback) diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index f6f976f213ac02..44498ef790e450 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2116,46 +2116,6 @@ def test_shield_cancel_outer(self): self.assertTrue(outer.cancelled()) self.assertEqual(0, 0 if outer._callbacks is None else len(outer._callbacks)) - def test_shield_cancel_outer_result(self): - mock_handler = mock.Mock() - self.loop.set_exception_handler(mock_handler) - inner = self.new_future(self.loop) - outer = asyncio.shield(inner) - test_utils.run_briefly(self.loop) - outer.cancel() - test_utils.run_briefly(self.loop) - inner.set_result(1) - test_utils.run_briefly(self.loop) - mock_handler.assert_not_called() - - def test_shield_cancel_outer_exception(self): - mock_handler = mock.Mock() - self.loop.set_exception_handler(mock_handler) - inner = self.new_future(self.loop) - outer = asyncio.shield(inner) - test_utils.run_briefly(self.loop) - outer.cancel() - test_utils.run_briefly(self.loop) - inner.set_exception(Exception('foo')) - test_utils.run_briefly(self.loop) - mock_handler.assert_called_once() - - def test_shield_duplicate_log_once(self): - mock_handler = mock.Mock() - self.loop.set_exception_handler(mock_handler) - inner = self.new_future(self.loop) - outer = asyncio.shield(inner) - test_utils.run_briefly(self.loop) - outer.cancel() - test_utils.run_briefly(self.loop) - outer = asyncio.shield(inner) - test_utils.run_briefly(self.loop) - outer.cancel() - test_utils.run_briefly(self.loop) - inner.set_exception(Exception('foo')) - test_utils.run_briefly(self.loop) - mock_handler.assert_called_once() - def test_shield_shortcut(self): fut = self.new_future(self.loop) fut.set_result(42) diff --git a/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst b/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst deleted file mode 100644 index d3117b16f04436..00000000000000 --- a/Misc/NEWS.d/next/Library/2025-05-20-15-13-43.gh-issue-86802.trF7TM.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed asyncio memory leak in cancelled shield tasks. For shielded tasks -where the shield was cancelled, log potential exceptions through the -exception handler. Contributed by Christian Harries. From a78bde0607df475e3ac009485ec22afba8d05e59 Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:33:34 +0100 Subject: [PATCH 10/12] Fix connection backlog offset + updated tests + added test for backlog=0 and backlog=1 --- Lib/asyncio/selector_events.py | 2 +- Lib/test/test_asyncio/test_selector_events.py | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/selector_events.py b/Lib/asyncio/selector_events.py index 22147451fa7ebd..6ad84044adf146 100644 --- a/Lib/asyncio/selector_events.py +++ b/Lib/asyncio/selector_events.py @@ -173,7 +173,7 @@ def _accept_connection( # listening socket has triggered an EVENT_READ. There may be multiple # connections waiting for an .accept() so it is called in a loop. # See https://bugs.python.org/issue27906 for more details. - for _ in range(backlog): + for _ in range(backlog + 1): try: conn, addr = sock.accept() if self._debug: diff --git a/Lib/test/test_asyncio/test_selector_events.py b/Lib/test/test_asyncio/test_selector_events.py index de81936b7456f2..aab6a779170eb9 100644 --- a/Lib/test/test_asyncio/test_selector_events.py +++ b/Lib/test/test_asyncio/test_selector_events.py @@ -347,6 +347,18 @@ def test_process_events_write_cancelled(self): selectors.EVENT_WRITE)]) self.loop._remove_writer.assert_called_with(1) + def test_accept_connection_zero_one(self): + for backlog in [0, 1]: + sock = mock.Mock() + sock.accept.return_value = (mock.Mock(), mock.Mock()) + with self.subTest(backlog): + mock_obj = mock.patch.object + with mock_obj(self.loop, '_accept_connection2') as accept2_mock: + self.loop._accept_connection( + mock.Mock(), sock, backlog=backlog) + self.loop.run_until_complete(asyncio.sleep(0)) + self.assertEqual(sock.accept.call_count, backlog + 1) + def test_accept_connection_multiple(self): sock = mock.Mock() sock.accept.return_value = (mock.Mock(), mock.Mock()) @@ -362,7 +374,7 @@ def test_accept_connection_multiple(self): self.loop._accept_connection( mock.Mock(), sock, backlog=backlog) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) def test_accept_connection_skip_connectionabortederror(self): sock = mock.Mock() @@ -388,7 +400,7 @@ def mock_sock_accept(): # as in test_accept_connection_multiple avoid task pending # warnings by using asyncio.sleep(0) self.loop.run_until_complete(asyncio.sleep(0)) - self.assertEqual(sock.accept.call_count, backlog) + self.assertEqual(sock.accept.call_count, backlog + 1) class SelectorTransportTests(test_utils.TestCase): From f710535b4ac047027b9fb96d92b90700fb5cf925 Mon Sep 17 00:00:00 2001 From: Chris33871 Date: Tue, 20 May 2025 21:46:26 +0100 Subject: [PATCH 11/12] added blurb --- .../next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst b/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst new file mode 100644 index 00000000000000..12026535720ff4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst @@ -0,0 +1,2 @@ +Fixed an off by one error concerning the backlog parameter in +asyncio.create_unix_server. Contributed by Christian Harries. From 0f28986c47bfcd1822d8ff24f3551ab7bc725af2 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Wed, 21 May 2025 17:28:34 +0530 Subject: [PATCH 12/12] Update Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst --- .../next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst b/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst index 12026535720ff4..49397c9705ecfe 100644 --- a/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst +++ b/Misc/NEWS.d/next/Library/2025-05-20-21-45-58.gh-issue-90871.Gkvtp6.rst @@ -1,2 +1,2 @@ Fixed an off by one error concerning the backlog parameter in -asyncio.create_unix_server. Contributed by Christian Harries. +:meth:`~asyncio.loop.create_unix_server`. Contributed by Christian Harries.