You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Counter-intuitive behavior of Server.close() / wait_closed() #79033
Is it correct for Server.wait_closed() (as implemented in asyncio) to be a no-op after Server.close()?
How can I tell that all incoming connections have been received by connection_made() after Server.close()?
**Details**
After calling Server.close(), _sockets is None, which makes Server.wait_closed() a no-op: it returns immediately without doing anything (as mentioned in https://bugs.python.org/issue33727).
I'm not sure why the docs suggest to call wait_closed() after close() if it's a no-op. My best guess is: "this design supports third-party event loops that requires an asynchronous API for closing servers, but the built-in event loops don't need that". Does someone know?
I wrote a very simple server that merely accepts connections. I ran experiments where I saturate the server with incoming client connections and close it. I checked what happens around close() (and wait_closed() -- but as it doesn't do anything after close() I'll just say close() from now on.)
The current implementation appears to work as documented, assuming an rather low level interpretation of the docs of Server.close().
Stop serving: close listening sockets and set the sockets attribute to None.
Correct -- I'm not seeing any accept calls in BaseSelectorEventLoop._accept_connection after close().
The sockets that represent existing incoming client connections are left open.
Correct -- if "existing incoming client connections" is interpreted as "client connections that have gone through accept".
The server is closed asynchronously, use the wait_closed() coroutine to wait until the server is closed.
I'm seeing calls to connection_made()afterclose() because BaseSelectorEventLoop._accept_connection2 triggers connection_made() asynchronously with call_soon().
This is surprising for someone approaching asyncio from the public API rather than the internal implementation. connection_made() is the first contact with new connections. The concept of "an existing incoming client connection for which connection_made() wasn't called yet" is unexpected.
This has practical consequences.
Consider a server that keeps track of established connections via connection_made and connection_lost. If this server calls Server.close(), awaits Server.wait_closed(), makes a list of established connections and terminates them, there's no guarantee that all connections will be closed. Indeed, new connections may appear and call connection_made() after close() and wait_closed() returned!
wait_closed() seems ineffective for this use case.
# Workaround for wait_closed() not quite doing
# what I want.
await asyncio.sleep(0)
# I believe that all accepted connections have reached
# connection_made() at this point.
aymericaugustin commented on May 30, 2021
aymericaugustinmannequin
MannequinAuthor
More actions
Would it make sense to add await asyncio.sleep(0) in Server.wait_closed() to ensure that all connections reach connection_made() before wait_closed() returns?
This would be fragile but it would be an improvement over the current behavior, wouldn't it?
I think I got it (GH-79033). I don't think we should backport this, even though it's fixing a bug -- code that got the close sequence wrong but managed to get away with it might suddenly hang.
I need to think about a test for the correct behavior.
I think I have a fix. The check at the top of wait_closed() should actually be
ifself._waitersisNone:
return
This makes it block until both conditions are true, as was the intended semantics (that we never quite got right): the server is closed and there are no active connections left. This new, simpler, check works because _wakeup() is the only place that sets _waiters to None, and it is called exactly when both conditions have become true (either in _detach() or in close()).
Activity
aymericaugustin commentedon Sep 30, 2018
**Summary**
Server.wait_closed()(as implemented in asyncio) to be a no-op afterServer.close()?connection_made()afterServer.close()?**Details**
After calling
Server.close(),_sockets is None, which makesServer.wait_closed()a no-op: it returns immediately without doing anything (as mentioned in https://bugs.python.org/issue33727).I'm not sure why the docs suggest to call
wait_closed()afterclose()if it's a no-op. My best guess is: "this design supports third-party event loops that requires an asynchronous API for closing servers, but the built-in event loops don't need that". Does someone know?I wrote a very simple server that merely accepts connections. I ran experiments where I saturate the server with incoming client connections and close it. I checked what happens around
close()(andwait_closed()-- but as it doesn't do anything afterclose()I'll just sayclose()from now on.)The current implementation appears to work as documented, assuming an rather low level interpretation of the docs of
Server.close().Correct -- I'm not seeing any
acceptcalls inBaseSelectorEventLoop._accept_connectionafterclose().Correct -- if "existing incoming client connections" is interpreted as "client connections that have gone through
accept".I'm seeing calls to
connection_made()afterclose()becauseBaseSelectorEventLoop._accept_connection2triggersconnection_made()asynchronously withcall_soon().This is surprising for someone approaching asyncio from the public API rather than the internal implementation.
connection_made()is the first contact with new connections. The concept of "an existing incoming client connection for whichconnection_made()wasn't called yet" is unexpected.This has practical consequences.
Consider a server that keeps track of established connections via
connection_madeandconnection_lost. If this server callsServer.close(), awaitsServer.wait_closed(), makes a list of established connections and terminates them, there's no guarantee that all connections will be closed. Indeed, new connections may appear and callconnection_made()afterclose()andwait_closed()returned!wait_closed()seems ineffective for this use case.aymericaugustin commentedon Sep 30, 2018
For now I will use the following hack:
aymericaugustin commentedon May 30, 2021
Would it make sense to add
await asyncio.sleep(0)inServer.wait_closed()to ensure that all connections reachconnection_made()beforewait_closed()returns?This would be fragile but it would be an improvement over the current behavior, wouldn't it?
kumaraditya303 commentedon Oct 12, 2022
IMO it should wait for all connections to be closed and the current behavior seems like an oversight.
gvanrossum commentedon Oct 23, 2022
Yeah, there seem to be an unfinished thought here. Some of the logic I've discovered:
self._waitersiswait_closed()itself_attach()/_detach()serve as a kind of reference count, counting transports_SelectorTransportand_ProactorBasePipeTransport_detachis called when the connection is lost, and wakes up thewait_closed()call when the count reaches zeroIt does seem likely that
wait_closed()just shouldn't check forself._socketsand then all will be well?Somebody should give this a try.
CC: @aaugustin @1st1
gvanrossum commentedon Oct 24, 2022
I think I got it (GH-79033). I don't think we should backport this, even though it's fixing a bug -- code that got the close sequence wrong but managed to get away with it might suddenly hang.
I need to think about a test for the correct behavior.
GH-79033: Fix asyncio.Server.wait_closed() (#98582)
13 remaining items
gvanrossum commentedon Oct 25, 2023
I think I have a fix. The check at the top of
wait_closed()should actually beThis makes it block until both conditions are true, as was the intended semantics (that we never quite got right): the server is closed and there are no active connections left. This new, simpler, check works because
_wakeup()is the only place that sets_waiterstoNone, and it is called exactly when both conditions have become true (either in_detach()or inclose()).PR forthcoming.
Dreamsorcerer commentedon Oct 26, 2023
Sounds good. Another option may have been to have an asyncio.Event to signal when the server is closed, and then it could simply await on that.
gvanrossum commentedon Oct 26, 2023
Yeah, but internally an Event is implemented similarly, and this code was already there… Can you approve the PR?
gh-79033: Try to fix asyncio.Server.wait_closed() again (GH-111336)
pythongh-79033: Try to fix asyncio.Server.wait_closed() again (python…
[3.12] gh-79033: Try to fix asyncio.Server.wait_closed() again (GH-11…
pythongh-79033: Try to fix asyncio.Server.wait_closed() again (python…
Ensure proper cleanup in MockImapServer
Close writers in test
encukou commentedon Feb 6, 2024
The latest PR is merged. Is there more to do in this issue?
gvanrossum commentedon Feb 6, 2024
I think it's done. Closing.
pythongh-79033: Try to fix asyncio.Server.wait_closed() again (python…
pythongh-79033: Try to fix asyncio.Server.wait_closed() again (python…