From 42db293616a5b7b49e40a045d71edf965530ed96 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Thu, 2 Nov 2023 13:48:49 +0530 Subject: [PATCH 1/2] [3.12] GH-110894: Call loop exception handler for exceptions in client_connected_cb (GH-111601) (GH-111632) (cherry picked from commit 9aa88290d82e2808eed84e7a63d0bf9623f84f53) Co-authored-by: Kumar Aditya Call loop exception handler for exceptions in `client_connected_cb` of `asyncio.start_server` so that applications can handle it.. (cherry picked from commit 229f44d353c71185414a072017f46f125676bdd6) --- Lib/asyncio/streams.py | 12 ++++++++ Lib/test/test_asyncio/test_streams.py | 29 +++++++++++++++++++ ...-11-01-14-03-24.gh-issue-110894.7-wZxC.rst | 1 + 3 files changed, 42 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-11-01-14-03-24.gh-issue-110894.7-wZxC.rst diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index 19d9154b66d027..7c407067e05a16 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -245,7 +245,19 @@ def connection_made(self, transport): res = self._client_connected_cb(reader, self._stream_writer) if coroutines.iscoroutine(res): + def callback(task): + exc = task.exception() + if exc is not None: + self._loop.call_exception_handler({ + 'message': 'Unhandled exception in client_connected_cb', + 'exception': exc, + 'transport': transport, + }) + transport.close() + self._task = self._loop.create_task(res) + self._task.add_done_callback(callback) + self._strong_reader = None def connection_lost(self, exc): diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index 95fc7a159baee8..b80b2e09718028 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -1068,6 +1068,35 @@ def test_eof_feed_when_closing_writer(self): self.assertEqual(messages, []) + def test_unhandled_exceptions(self) -> None: + port = socket_helper.find_unused_port() + + messages = [] + self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx)) + + async def client(): + rd, wr = await asyncio.open_connection('localhost', port) + wr.write(b'test msg') + await wr.drain() + wr.close() + await wr.wait_closed() + + async def main(): + async def handle_echo(reader, writer): + raise Exception('test') + + server = await asyncio.start_server( + handle_echo, 'localhost', port) + await server.start_serving() + await client() + server.close() + await server.wait_closed() + + self.loop.run_until_complete(main()) + + self.assertEqual(messages[0]['message'], + 'Unhandled exception in client_connected_cb') + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2023-11-01-14-03-24.gh-issue-110894.7-wZxC.rst b/Misc/NEWS.d/next/Library/2023-11-01-14-03-24.gh-issue-110894.7-wZxC.rst new file mode 100644 index 00000000000000..c59fe6b9119eca --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-01-14-03-24.gh-issue-110894.7-wZxC.rst @@ -0,0 +1 @@ +Call loop exception handler for exceptions in ``client_connected_cb`` of :func:`asyncio.start_server` so that applications can handle it. Patch by Kumar Aditya. From dced4b8e0d349d0943c0d244270873817428d637 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 4 Nov 2023 01:47:07 +0100 Subject: [PATCH 2/2] gh-111644: Fix asyncio test_unhandled_exceptions() (#111713) Fix test_unhandled_exceptions() of test_asyncio.test_streams: break explicitly a reference cycle. Fix also StreamTests.tearDown(): the loop must not be closed explicitly, but using set_event_loop() which takes care of shutting down the executor with executor.shutdown(wait=True). BaseEventLoop.close() calls executor.shutdown(wait=False). (cherry picked from commit ac01e2243a1104b2154c0d1bdbc9f8d5b3ada778) --- Lib/test/test_asyncio/test_streams.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index b80b2e09718028..86354306f1fff3 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -36,8 +36,7 @@ def tearDown(self): # just in case if we have transport close callbacks test_utils.run_briefly(self.loop) - self.loop.close() - gc.collect() + # set_event_loop() takes care of closing self.loop in a safe way super().tearDown() def _basetest_open_connection(self, open_connection_fut): @@ -1096,6 +1095,8 @@ async def handle_echo(reader, writer): self.assertEqual(messages[0]['message'], 'Unhandled exception in client_connected_cb') + # Break explicitly reference cycle + messages = None if __name__ == '__main__':