Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions Doc/library/socketserver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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()

Expand All @@ -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()

Expand Down Expand Up @@ -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`.
Expand Down
49 changes: 43 additions & 6 deletions Lib/socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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.

Expand Down
42 changes: 37 additions & 5 deletions Lib/test/test_socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import threading
import unittest
import socketserver
import time

import test.support
from test.support import reap_children, verbose
Expand Down Expand Up @@ -118,22 +119,24 @@ 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")
for i in range(3):
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):
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Loading