gh-113538: Add asycio.Server.{close,abort}_clients (#114432)

These give applications the option of more forcefully terminating client
connections for asyncio servers. Useful when terminating a service and
there is limited time to wait for clients to finish up their work.
This commit is contained in:
Pierre Ossman (ThinLinc team) 2024-03-11 20:43:30 +01:00 committed by GitHub
parent 872c0714fc
commit 1d0d49a7e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 152 additions and 20 deletions

View file

@ -279,7 +279,9 @@ class Server(events.AbstractServer):
ssl_handshake_timeout, ssl_shutdown_timeout=None):
self._loop = loop
self._sockets = sockets
self._active_count = 0
# Weak references so we don't break Transport's ability to
# detect abandoned transports
self._clients = weakref.WeakSet()
self._waiters = []
self._protocol_factory = protocol_factory
self._backlog = backlog
@ -292,14 +294,13 @@ class Server(events.AbstractServer):
def __repr__(self):
return f'<{self.__class__.__name__} sockets={self.sockets!r}>'
def _attach(self):
def _attach(self, transport):
assert self._sockets is not None
self._active_count += 1
self._clients.add(transport)
def _detach(self):
assert self._active_count > 0
self._active_count -= 1
if self._active_count == 0 and self._sockets is None:
def _detach(self, transport):
self._clients.discard(transport)
if len(self._clients) == 0 and self._sockets is None:
self._wakeup()
def _wakeup(self):
@ -348,9 +349,17 @@ class Server(events.AbstractServer):
self._serving_forever_fut.cancel()
self._serving_forever_fut = None
if self._active_count == 0:
if len(self._clients) == 0:
self._wakeup()
def close_clients(self):
for transport in self._clients.copy():
transport.close()
def abort_clients(self):
for transport in self._clients.copy():
transport.abort()
async def start_serving(self):
self._start_serving()
# Skip one loop iteration so that all 'loop.add_reader'

View file

@ -175,6 +175,14 @@ class AbstractServer:
"""Stop serving. This leaves existing connections open."""
raise NotImplementedError
def close_clients(self):
"""Close all active connections."""
raise NotImplementedError
def abort_clients(self):
"""Close all active connections immediately."""
raise NotImplementedError
def get_loop(self):
"""Get the event loop the Server object is attached to."""
raise NotImplementedError

View file

@ -63,7 +63,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
self._called_connection_lost = False
self._eof_written = False
if self._server is not None:
self._server._attach()
self._server._attach(self)
self._loop.call_soon(self._protocol.connection_made, self)
if waiter is not None:
# only wake up the waiter when connection_made() has been called
@ -167,7 +167,7 @@ class _ProactorBasePipeTransport(transports._FlowControlMixin,
self._sock = None
server = self._server
if server is not None:
server._detach()
server._detach(self)
self._server = None
self._called_connection_lost = True

View file

@ -791,7 +791,7 @@ class _SelectorTransport(transports._FlowControlMixin,
self._paused = False # Set when pause_reading() called
if self._server is not None:
self._server._attach()
self._server._attach(self)
loop._transports[self._sock_fd] = self
def __repr__(self):
@ -868,6 +868,8 @@ class _SelectorTransport(transports._FlowControlMixin,
if self._sock is not None:
_warn(f"unclosed transport {self!r}", ResourceWarning, source=self)
self._sock.close()
if self._server is not None:
self._server._detach(self)
def _fatal_error(self, exc, message='Fatal error on transport'):
# Should be called from exception handler only.
@ -906,7 +908,7 @@ class _SelectorTransport(transports._FlowControlMixin,
self._loop = None
server = self._server
if server is not None:
server._detach()
server._detach(self)
self._server = None
def get_write_buffer_size(self):