]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-123720: When closing an asyncio server, stop the handlers (#124689)
authorGuido van Rossum <guido@python.org>
Sat, 14 Mar 2026 18:28:49 +0000 (11:28 -0700)
committerGitHub <noreply@github.com>
Sat, 14 Mar 2026 18:28:49 +0000 (11:28 -0700)
Lib/asyncio/base_events.py
Lib/test/test_asyncio/test_server.py
Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst [new file with mode: 0644]

index 0930ef403c6c4be495224b10e052be94dc482885..77c70aaa7b986e15402342a571fb14c9664d13f1 100644 (file)
@@ -381,6 +381,7 @@ class Server(events.AbstractServer):
         except exceptions.CancelledError:
             try:
                 self.close()
+                self.close_clients()
                 await self.wait_closed()
             finally:
                 raise
index 5bd0f7e2af4f8495737727f2c9d78a6f8cba9d3f..581ea47d2dec976e3b5315578248676fef63bd96 100644 (file)
@@ -266,6 +266,38 @@ class TestServer2(unittest.IsolatedAsyncioTestCase):
         await asyncio.sleep(0)
         self.assertTrue(task.done())
 
+    async def test_close_with_hanging_client(self):
+        # Synchronize server cancellation only after the socket connection is
+        # accepted and this event is set
+        conn_event = asyncio.Event()
+        class Proto(asyncio.Protocol):
+            def connection_made(self, transport):
+                conn_event.set()
+
+        loop = asyncio.get_running_loop()
+        srv = await loop.create_server(Proto, socket_helper.HOSTv4, 0)
+
+        # Start the server
+        serve_forever_task = asyncio.create_task(srv.serve_forever())
+        await asyncio.sleep(0)
+
+        # Create a connection to server
+        addr = srv.sockets[0].getsockname()
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.connect(addr)
+        self.addCleanup(sock.close)
+
+        # Send a CancelledError into the server to emulate a Ctrl+C
+        # KeyboardInterrupt whilst the server is handling a hanging client
+        await conn_event.wait()
+        serve_forever_task.cancel()
+
+        # Ensure the client is closed within a timeout
+        async with asyncio.timeout(0.5):
+            await srv.wait_closed()
+
+        self.assertFalse(srv.is_serving())
+
 
 # Test the various corner cases of Unix server socket removal
 class UnixServerCleanupTests(unittest.IsolatedAsyncioTestCase):
diff --git a/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst b/Misc/NEWS.d/next/Library/2026-03-11-10-25-32.gh-issue-123720.TauFRx.rst
new file mode 100644 (file)
index 0000000..04e6a37
--- /dev/null
@@ -0,0 +1,5 @@
+asyncio: Fix :func:`asyncio.Server.serve_forever` shutdown regression. Since
+3.12, cancelling ``serve_forever()`` could hang waiting for a handler blocked
+on a read from a client that never closed (effectively requiring two
+interrupts to stop); the shutdown sequence now ensures client streams are
+closed so ``serve_forever()`` exits promptly and handlers observe EOF.