Repository navigation
Race condition in asyncio.Server.close #109564
Description
Bug report
Bug description:
There is a race condition when closing an asyncio.Server. Accepting a connection takes several iterations of the event loop to complete, and a call to Server.close in the middle will lead to exceptions.
First, some demonstration code. Run both of these concurrently, running the server with python -X dev:
Client:
#!/usr/bin/env python3
import socket
while True:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.connect(("127.0.0.1", 4445))
except IOError:
passServer:
#!/usr/bin/env python3
import asyncio
def cb(reader, writer):
writer.close()
def _my_attach(self):
if self._sockets is None:
print(self._sockets, self._active_count, flush=True)
_orig_attach(self)
async def main():
while True:
server = await asyncio.start_server(cb, host="127.0.0.1", port=4445)
await asyncio.sleep(0)
server.close()
await server.wait_closed()
asyncio.run(main())It will repeatly output errors like this:
Error on transport creation for incoming connection
handle_traceback: Handle created at (most recent call last):
File "/usr/lib/python3.12/asyncio/runners.py", line 194, in run
return runner.run(main)
File "/usr/lib/python3.12/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
File "/usr/lib/python3.12/asyncio/base_events.py", line 651, in run_until_complete
self.run_forever()
File "/usr/lib/python3.12/asyncio/base_events.py", line 618, in run_forever
self._run_once()
File "/usr/lib/python3.12/asyncio/base_events.py", line 1943, in _run_once
handle._run()
File "/usr/lib/python3.12/asyncio/events.py", line 84, in _run
self._context.run(self._callback, *self._args)
File "/usr/lib/python3.12/asyncio/selector_events.py", line 211, in _accept_connection
self.create_task(accept)
File "/usr/lib/python3.12/asyncio/base_events.py", line 436, in create_task
task = tasks.Task(coro, loop=self, name=name, context=context)
protocol: <asyncio.streams.StreamReaderProtocol object at 0x7f7bb340b350>
Traceback (most recent call last):
File "/usr/lib/python3.12/asyncio/selector_events.py", line 230, in _accept_connection2
transport = self._make_socket_transport(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/selector_events.py", line 72, in _make_socket_transport
return _SelectorSocketTransport(self, sock, protocol, waiter,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/asyncio/selector_events.py", line 936, in __init__
super().__init__(loop, sock, protocol, extra, server)
File "/usr/lib/python3.12/asyncio/selector_events.py", line 800, in __init__
self._server._attach()
File "/usr/lib/python3.12/asyncio/base_events.py", line 294, in _attach
assert self._sockets is not None
AssertionError
Additionally, stopping the server (with Ctrl-C) prints lots of ResourceWarnings about unclosed transports and sockets.
When a connection arrives on a socket (with selector_events), it goes through the following steps before reaching Server._attach:
- The socket is accepted: here
_accept_connection2is scheduled (will only run on next event loop iteration): here- The transport is created: here
Server._attachis called: here
It's possible for server.close() to be executed between steps 2 and 3, in which case Server._sockets has already been set to None, and the assertion is triggered. The client will presumably experience this as a connection that either closes immediately (if the garbage collector closes the socket) or is unresponsive.
This particular bug might be fixable by creating the transport synchronously in _accept_connection. I think there may be further potential race conditions because connection_made is also called asynchronously, which means it is possible for it to be called after Server.close has already returned. That might not break anything in asyncio itself but it will cause software that does something like the following to hang in 3.12 (related to #79033 / #104344):
server.close()
for (reader, writer) in my_connections:
writer.close()
await writer.closed()
await server.wait_closed()CPython versions tested on:
3.11, 3.12
Operating systems tested on:
Linux
Linked PRs
- gh-109564: Add
Nonecheck forasyncio.Server._wakeup#126660 - GH-109564: add asyncio.Server state machine #131009
- gh-109564: check server._sockets before attaching a new transport #154861
- gh-109564: asyncio: drop connections accepted by a closed server #160851
- [3.15] gh-109564: asyncio: drop connections accepted by a closed server (GH-160851) #160878
- [3.14] gh-109564: asyncio: drop connections accepted by a closed server (GH-160851) #160879
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Activity
Yaro1 commentedon Nov 29, 2023
Hi, I would like to take that issue!
gvanrossum commentedon Dec 1, 2023
@Yaro1 Okay, first step, can you reproduce the issue locally using the
mainbranch?Yaro1 commentedon Dec 1, 2023
@gvanrossum Yes I reproduced
gvanrossum commentedon Dec 2, 2023
Okay go ahead and work on a fix then. Come back here if you are stuck.
Yaro1 commentedon Dec 2, 2023
okay, thank you
Yaro1 commentedon Dec 9, 2023
Well, I'm not sure about creating the transport synchronously in _accept_connection.
I tried to understand what is the reason of dividing accepting socket and creating a transport to _accept_connection and _accept_connection2.
I found commit which include creating _accept_connection2 2934262
But there is a problem - I guess there is not enough information in commit message:
And the link for now indicates to another issue.
Could you help me please with an understanding of purpose _accept_connection and _accept_connection2? @gvanrossum
gvanrossum commentedon Dec 10, 2023
I'm sorry, I recommend that you try a simpler issue.
Yaro1 commentedon Dec 10, 2023
okay I will take simpler
ordinary-jamie commentedon Nov 11, 2024
Hello @bmerry 👋 I was able to reproduce this issue on latest;
repro code
As noted in this issue, the reader callback for the listening socket (
_accept_connection) and the coroutine scheduled for the accepted connection are separate (_accept_connection2). This means thesocketserver can be closed in the period between the_accept_connection2coroutine being scheduled and the coroutine actually running.However, I don't believe a code change is needed to address this, since the transport will be closed on error and throw a
CancelledErrorinto_accept_connection2; for this edge-case I would considered that sufficiently handled. And, the assertions will only be logged, and that should be turned off by clearingPYTHONASYNCIODEBUGor withpython -O. What do you think?Yup, I see 3 separate clean-up issues:
BaseSelectorEventLoop._accept_connection2to close the socket. I'm a little hesitant to add this however, as I'm not sure if this will cause aEBADFif the socket gets closed twice in the happy path; what do you think, @gvanrossum? Would something likeif not transport: conn.close()be necessary?_SelectorTransport.__del__: I don't believe it is necessary to handle theseunclosed transportwarnings; I think this is working as intended.Server._wakeup: Theasyncio.Serverhas a missingNonecheck for when it wants to set its waiters; this happens in this edge-case because there is an explicitServer.closecall in addition to the transports detaching from the server, resulting in multiple calls toServer._wakeup; I've made PR gh-109564: AddNonecheck forasyncio.Server._wakeup#126660 to address thisasvetlov commentedon Nov 11, 2024
Multiple
sock.close()calls don't raise an exception;conn.close()could be called explicitly in bothexcept (SystemExit, KeyboardInterrupt)andexcept BaseExceptionbranches safely.ordinary-jamie commentedon Nov 11, 2024
Thanks @asvetlov; should I put this change in a different PR to #126660, since the
Nonecheck and the socket close seem unrelated to each other?zero-context commentedon Jul 28, 2026
gvanrossum commentedon Jul 28, 2026
That was obviously AI-generated. It's too long to read.
gh-109564: asyncio: drop connections accepted by a closed server (#16…
[3.14] gh-109564: asyncio: drop connections accepted by a closed serv…
[3.15] gh-109564: asyncio: drop connections accepted by a closed serv…
asvetlov commentedon Oct 29, 2026
Fixed, thank you all