diff --git a/Doc/library/socketserver.rst b/Doc/library/socketserver.rst index 59cfa136a3b7da..95c1cf6bc7c92f 100644 --- a/Doc/library/socketserver.rst +++ b/Doc/library/socketserver.rst @@ -230,7 +230,7 @@ Server Objects will return. - .. method:: serve_forever(poll_interval=0.5) + .. method:: serve_forever(poll_interval=30) Handle requests until an explicit :meth:`shutdown` request. Poll for shutdown every *poll_interval* seconds. @@ -243,6 +243,9 @@ Server Objects .. versionchanged:: 3.3 Added ``service_actions`` call to the ``serve_forever`` method. + .. versionchanged:: 3.14 + Default of *poll_interval* is now ``30`` instead of ``0.5``. + .. method:: service_actions() @@ -252,12 +255,16 @@ Server Objects .. versionadded:: 3.3 - .. method:: shutdown() + .. method:: shutdown(write_to_self=True) Tell the :meth:`serve_forever` loop to stop and wait until it does. + If *write_to_self* is True, :meth:`write_to_self` called + and the loop will wake up immediately if server is not closed. :meth:`shutdown` must be called while :meth:`serve_forever` is running in a different thread otherwise it will deadlock. + .. versionchanged:: 3.14 + Added ``write_to_self`` parameter. .. method:: server_close() @@ -399,6 +406,14 @@ Server Objects default implementation always returns :const:`True`. + .. method:: write_to_self() + + Connect to the server's socket and send null byte. + May be overriden. + + .. versionadded:: 3.14 + + .. versionchanged:: 3.6 Support for the :term:`context manager` protocol was added. Exiting the context manager is equivalent to calling :meth:`server_close`. diff --git a/Lib/socketserver.py b/Lib/socketserver.py index 35b2723de3babe..9584f44af4b15d 100644 --- a/Lib/socketserver.py +++ b/Lib/socketserver.py @@ -206,6 +206,8 @@ def __init__(self, server_address, RequestHandlerClass): self.RequestHandlerClass = RequestHandlerClass self.__is_shut_down = threading.Event() self.__shutdown_request = False + self.__not_shutting_down = threading.Event() + self.__not_shutting_down.set() def server_activate(self): """Called by constructor to activate the server. @@ -215,7 +217,7 @@ def server_activate(self): """ pass - def serve_forever(self, poll_interval=0.5): + def serve_forever(self, poll_interval=30): """Handle one request at a time until shutdown. Polls for shutdown every poll_interval seconds. Ignores @@ -224,10 +226,6 @@ def serve_forever(self, poll_interval=0.5): """ self.__is_shut_down.clear() try: - # XXX: Consider using another file descriptor or connecting to the - # socket to wake this up instead of polling. Polling reduces our - # responsiveness to a shutdown request and wastes cpu at all other - # times. with _ServerSelector() as selector: selector.register(self, selectors.EVENT_READ) @@ -242,18 +240,57 @@ def serve_forever(self, poll_interval=0.5): self.service_actions() finally: self.__shutdown_request = False + self.__not_shutting_down.wait() self.__is_shut_down.set() - def shutdown(self): + def shutdown(self, write_to_self=True): """Stops the serve_forever loop. + If write_to_self is True, write_to_self() will be called + and the loop will wake up immediately if server is not closed. + Blocks until the loop has finished. This must be called while serve_forever() is running in another thread, or it will deadlock. """ self.__shutdown_request = True + + if write_to_self: + # On Windows connecting to a closed server takes a long time. + # So, assuming that server_close() is called right after + # serve_forever() returns, we will make loop wait + # for write_to_self() to complete. + self.__not_shutting_down.clear() + try: + if self.__is_shut_down.is_set(): + return + self.write_to_self() + finally: + self.__not_shutting_down.set() + self.__is_shut_down.wait() + def write_to_self(self): + """Connect to the server's socket and send null byte. + + May be overriden. + """ + try: + addr = self.socket.getsockname() + except OSError: + return # Socket is closed + + try: + with socket.socket(self.socket.family, self.socket.type) as sock: + sock.connect(addr) + sock.setblocking(False) + try: + sock.send(b'\0') + except BlockingIOError: + return + except ConnectionRefusedError: + pass # Server closed before we connected + def service_actions(self): """Called by the serve_forever() loop. diff --git a/Lib/test/test_socketserver.py b/Lib/test/test_socketserver.py index 0f62f9eb200e42..19ac69f5a4ee62 100644 --- a/Lib/test/test_socketserver.py +++ b/Lib/test/test_socketserver.py @@ -11,6 +11,7 @@ import threading import unittest import socketserver +import time import test.support from test.support import reap_children, verbose @@ -118,13 +119,12 @@ def run_server(self, svrcls, hdlrbase, testfunc): print("ADDR =", addr) print("CLASS =", svrcls) + # Short poll interval, if shutdown won't wake loop. + poll_interval = 1 t = threading.Thread( name='%s serving' % svrcls, target=server.serve_forever, - # Short poll interval to make the test finish quickly. - # Time between requests is short enough that we won't wake - # up spuriously too many times. - kwargs={'poll_interval':0.01}) + kwargs={'poll_interval': poll_interval}) t.daemon = True # In case this function raises. t.start() if verbose: print("server running") @@ -132,8 +132,11 @@ def run_server(self, svrcls, hdlrbase, testfunc): if verbose: print("test client", i) testfunc(svrcls.address_family, addr) if verbose: print("waiting for server") + start_time = time.perf_counter() server.shutdown() + time_taken = time.perf_counter() - start_time t.join() + self.assertLess(time_taken, poll_interval / 2) server.server_close() self.assertEqual(-1, server.socket.fileno()) if HAVE_FORKING and isinstance(server, socketserver.ForkingMixIn): @@ -252,7 +255,7 @@ class MyHandler(socketserver.StreamRequestHandler): t = threading.Thread( name='MyServer serving', target=s.serve_forever, - kwargs={'poll_interval':0.01}) + kwargs={'poll_interval': 1}) t.daemon = True # In case this function raises. threads.append((t, s)) for t, s in threads: @@ -262,6 +265,35 @@ class MyHandler(socketserver.StreamRequestHandler): t.join() s.server_close() + @threading_helper.reap_threads + def test_write_to_self_on_closed_server(self): + poll_interval = 0.1 + + class MyServer(socketserver.TCPServer): + def serve_forever(self, poll_interval=poll_interval) -> None: + try: + super().serve_forever(poll_interval) + except (ValueError, OSError): + # In case server was closed before select() was called + return + + class MyHandler(socketserver.StreamRequestHandler): + pass + + s = MyServer((HOST, 0), MyHandler) + t = threading.Thread( + name='MyServer serving', + target=s.serve_forever) + t.daemon = True # In case this function raises. + t.start() + s.server_close() + start_time = time.perf_counter() + s.write_to_self() # Should return without trying to connect + time_taken = time.perf_counter() - start_time + self.assertLess(time_taken, poll_interval / 10) + s.shutdown() + t.join() + def test_close_immediately(self): class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass diff --git a/Misc/NEWS.d/next/Library/2025-02-28-10-56-25.gh-issue-88932.QVit76.rst b/Misc/NEWS.d/next/Library/2025-02-28-10-56-25.gh-issue-88932.QVit76.rst new file mode 100644 index 00000000000000..73493a41219f45 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-02-28-10-56-25.gh-issue-88932.QVit76.rst @@ -0,0 +1,2 @@ +Add socketserver.BaseServer.write_to_self method, and call it from BaseServer.shutdown(). +Now serve_forever() loop wakes up immediately after shutdown() is called.