Repository navigation
Counter-intuitive behavior of Server.close() / wait_closed() #79033
Description
Activity
aymericaugustin commented
on Sep 30, 2018 aymericaugustinmannequinMannequinAuthorMore actions**Summary**
- Is it correct for
Server.wait_closed()(as implemented in asyncio) to be a no-op afterServer.close()? - How can I tell that all incoming connections have been received by
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().Stop serving: close listening sockets and set the sockets attribute to None.
Correct -- I'm not seeing any
acceptcalls inBaseSelectorEventLoop._accept_connectionafterclose().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()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.- Is it correct for
- added3.7 (EOL)end of lifeend of lifetype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Sep 30, 2018 aymericaugustin commented
on Sep 30, 2018 aymericaugustinmannequinMannequinAuthorMore actionsFor now I will use the following hack:
server.close() await server.wait_closed()
# 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 aymericaugustinmannequinMannequinAuthorMore actionsWould 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?
IMO it should wait for all connections to be closed and the current behavior seems like an oversight.
Yeah, there seem to be an unfinished thought here. Some of the logic I've discovered:
- The only waiter ever added to
self._waitersiswait_closed()itself _attach()/_detach()serve as a kind of reference count, counting transports- The only calls are in
_SelectorTransportand_ProactorBasePipeTransport _detachis called when the connection is lost, and wakes up thewait_closed()call when the count reaches zero
It 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
- The only waiter ever added to
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.
- added a commit that references this issue
on Nov 24, 2022 13 remaining items
I think I have a fix. The check at the top of
wait_closed()should actually beif self._waiters is None: 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_waiterstoNone, and it is called exactly when both conditions have become true (either in_detach()or inclose()).PR forthcoming.
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.
Yeah, but internally an Event is implemented similarly, and this code was already there… Can you approve the PR?
- added 2 commits that reference this issue
on Jan 7, 2024 The latest PR is merged. Is there more to do in this issue?
I think it's done. Closing.
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields:
bugs.python.org fields:
Linked PRs