Skip to content

Commit

Permalink
Issue python#21040: socketserver: Use the selectors module.
Browse files Browse the repository at this point in the history
  • Loading branch information
cf-natali committed Mar 24, 2014
1 parent e3fb80f commit 1d29cc5
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 75 deletions.
4 changes: 2 additions & 2 deletions Doc/library/socketserver.rst
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ the request handler class :meth:`handle` method.
Another approach to handling multiple simultaneous requests in an environment
that supports neither threads nor :func:`~os.fork` (or where these are too
expensive or inappropriate for the service) is to maintain an explicit table of
partially finished requests and to use :func:`~select.select` to decide which
partially finished requests and to use :mod:`selectors` to decide which
request to work on next (or whether to handle a new incoming request). This is
particularly important for stream services where each client can potentially be
connected for a long time (if threads or subprocesses cannot be used). See
Expand All @@ -136,7 +136,7 @@ Server Objects
.. method:: BaseServer.fileno()

Return an integer file descriptor for the socket on which the server is
listening. This function is most commonly passed to :func:`select.select`, to
listening. This function is most commonly passed to :mod:`selectors`, to
allow monitoring multiple servers in the same process.


Expand Down
98 changes: 57 additions & 41 deletions Lib/socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ class will essentially render the service "deaf" while one request is
Another approach to handling multiple simultaneous requests in an
environment that supports neither threads nor fork (or where these are
too expensive or inappropriate for the service) is to maintain an
explicit table of partially finished requests and to use select() to
explicit table of partially finished requests and to use a selector to
decide which request to work on next (or whether to handle a new
incoming request). This is particularly important for stream services
where each client can potentially be connected for a long time (if
Expand All @@ -104,7 +104,6 @@ class will essentially render the service "deaf" while one request is
- Standard classes for Sun RPC (which uses either UDP or TCP)
- Standard mix-in classes to implement various authentication
and encryption schemes
- Standard framework for select-based multiplexing
XXX Open problems:
- What to do with out-of-band data?
Expand All @@ -130,13 +129,17 @@ class will essentially render the service "deaf" while one request is


import socket
import select
import selectors
import os
import errno
try:
import threading
except ImportError:
import dummy_threading as threading
try:
from time import monotonic as time
except ImportError:
from time import time as time

__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
"ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
Expand All @@ -147,14 +150,13 @@ class will essentially render the service "deaf" while one request is
"ThreadingUnixStreamServer",
"ThreadingUnixDatagramServer"])

def _eintr_retry(func, *args):
"""restart a system call interrupted by EINTR"""
while True:
try:
return func(*args)
except OSError as e:
if e.errno != errno.EINTR:
raise
# poll/select have the advantage of not requiring any extra file descriptor,
# contrarily to epoll/kqueue (also, they require a single syscall).
if hasattr(selectors, 'PollSelector'):
_ServerSelector = selectors.PollSelector
else:
_ServerSelector = selectors.SelectSelector


class BaseServer:

Expand All @@ -166,7 +168,7 @@ class BaseServer:
- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request() # if you do not use serve_forever()
- fileno() -> int # for select()
- fileno() -> int # for selector
Methods that may be overridden:
Expand Down Expand Up @@ -227,17 +229,19 @@ def serve_forever(self, poll_interval=0.5):
"""
self.__is_shut_down.clear()
try:
while not self.__shutdown_request:
# 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.
r, w, e = _eintr_retry(select.select, [self], [], [],
poll_interval)
if self in r:
self._handle_request_noblock()

self.service_actions()
# 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)

while not self.__shutdown_request:
ready = selector.select(poll_interval)
if ready:
self._handle_request_noblock()

self.service_actions()
finally:
self.__shutdown_request = False
self.__is_shut_down.set()
Expand All @@ -260,16 +264,16 @@ def service_actions(self):
"""
pass

# The distinction between handling, getting, processing and
# finishing a request is fairly arbitrary. Remember:
# The distinction between handling, getting, processing and finishing a
# request is fairly arbitrary. Remember:
#
# - handle_request() is the top-level call. It calls
# select, get_request(), verify_request() and process_request()
# - handle_request() is the top-level call. It calls selector.select(),
# get_request(), verify_request() and process_request()
# - get_request() is different for stream or datagram sockets
# - process_request() is the place that may fork a new process
# or create a new thread to finish the request
# - finish_request() instantiates the request handler class;
# this constructor will handle the request all by itself
# - process_request() is the place that may fork a new process or create a
# new thread to finish the request
# - finish_request() instantiates the request handler class; this
# constructor will handle the request all by itself

def handle_request(self):
"""Handle one request, possibly blocking.
Expand All @@ -283,18 +287,30 @@ def handle_request(self):
timeout = self.timeout
elif self.timeout is not None:
timeout = min(timeout, self.timeout)
fd_sets = _eintr_retry(select.select, [self], [], [], timeout)
if not fd_sets[0]:
self.handle_timeout()
return
self._handle_request_noblock()
if timeout is not None:
deadline = time() + timeout

# Wait until a request arrives or the timeout expires - the loop is
# necessary to accomodate early wakeups due to EINTR.
with _ServerSelector() as selector:
selector.register(self, selectors.EVENT_READ)

while True:
ready = selector.select(timeout)
if ready:
return self._handle_request_noblock()
else:
if timeout is not None:
timeout = deadline - time()
if timeout < 0:
return self.handle_timeout()

def _handle_request_noblock(self):
"""Handle one request, without blocking.
I assume that select.select has returned that the socket is
readable before this function was called, so there should be
no risk of blocking in get_request().
I assume that selector.select() has returned that the socket is
readable before this function was called, so there should be no risk of
blocking in get_request().
"""
try:
request, client_address = self.get_request()
Expand Down Expand Up @@ -377,7 +393,7 @@ class TCPServer(BaseServer):
- serve_forever(poll_interval=0.5)
- shutdown()
- handle_request() # if you don't use serve_forever()
- fileno() -> int # for select()
- fileno() -> int # for selector
Methods that may be overridden:
Expand Down Expand Up @@ -459,7 +475,7 @@ def server_close(self):
def fileno(self):
"""Return socket file number.
Interface required by select().
Interface required by selector.
"""
return self.socket.fileno()
Expand Down
32 changes: 0 additions & 32 deletions Lib/test/test_socketserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,38 +222,6 @@ def test_ForkingUDPServer(self):
socketserver.DatagramRequestHandler,
self.dgram_examine)

@contextlib.contextmanager
def mocked_select_module(self):
"""Mocks the select.select() call to raise EINTR for first call"""
old_select = select.select

class MockSelect:
def __init__(self):
self.called = 0

def __call__(self, *args):
self.called += 1
if self.called == 1:
# raise the exception on first call
raise OSError(errno.EINTR, os.strerror(errno.EINTR))
else:
# Return real select value for consecutive calls
return old_select(*args)

select.select = MockSelect()
try:
yield select.select
finally:
select.select = old_select

def test_InterruptServerSelectCall(self):
with self.mocked_select_module() as mock_select:
pid = self.run_server(socketserver.TCPServer,
socketserver.StreamRequestHandler,
self.stream_examine)
# Make sure select was called again:
self.assertGreater(mock_select.called, 1)

# Alas, on Linux (at least) recvfrom() doesn't return a meaningful
# client address so this cannot work:

Expand Down

0 comments on commit 1d29cc5

Please sign in to comment.