Description
Bug report
Bug description:
Consider this code, slightly simplifying the documentation's TCPServer example code:
import asyncio
async def handle_echo(reader, writer):
print("Reading")
data = await reader.read(100)
message = data.decode()
print(f"Received '{message!r}'")
print("Closing the connection")
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(handle_echo, "127.0.0.1", 8888)
print("Serving forever...")
async with server:
try:
await server.serve_forever()
except asyncio.CancelledError:
print("Cancelled by Ctrl+C")
server.close()
asyncio.run(main())
(My code is closer to a while True: await reader.readline(); ...
, but the above probably suffices as a demonstration)
Running this results in a Serving forever, and hitting Ctrl+C, results in a Cancelled by Ctrl+C, and a normal exit.
However, if in another window we nc 127.0.0.1 8888
, and leave the connection open, Ctrl+C (SIGINT) does nothing, and a second Ctrl+C is required to terminate. (This however breaks out of the asyncio loop by raising KeyboardInterrupt()
, as documented).
So basically clients can prevent the server from cleanly exiting by just keeping their connection open.
This is a regression: this fails with 3.12.5 and 3.13.0-rc1 but works with 3.11.9.
This is because (TTBOMK) of this code in base_events.py
:
try:
await self._serving_forever_fut
except exceptions.CancelledError:
try:
self.close()
await self.wait_closed()
finally:
raise
finally:
self._serving_forever_fut = None
I believe this to be related to the wait_closed()
changes, 5d09d11, 2655369 etc. (Cc @gvanrossum). Related issues #104344 and #113538.
Per @gvanrossum in #113538 (comment): "In 3.10 and before, server.wait_closed() was a no-op, unless you called it before server.close(), in a task (asyncio.create_task(server.wait_closed())). The unclosed connection was just getting abandoned."
CancelledError()
is caught here, which spawns wait_closed()
before re-raising the exception. In 3.12+, wait_closed()
... actually waits for the connection to close, as intended. However, while this prevents the reader task from being abandoned, it does not allow neither the callers of reader.read()
or serve_forever()
to catch CancelledError()
and clean up (such as actually closing the connection, potentially after e.g. a signaling to the client a server close through whatever protocol is implemented here).
Basically no user code is executed until the client across the network drops the connection.
As far as I know, it's currently impossible to handle SIGINTs cleanly with clients blocked in a read()
without messing with deep asyncio/selector internals, which seems like a pretty serious limitation? Have I missed something?
CPython versions tested on:
3.11, 3.12, 3.13
Operating systems tested on:
Linux
Linked PRs
Metadata
Metadata
Assignees
Labels
Projects
Status
Status