X Tutup
Skip to content

Commit bd0bb00

Browse files
author
jeffrey.yasskin
committed
Progress on issue #1193577 by adding a polling .shutdown() method to
SocketServers. The core of the patch was written by Pedro Werneck, but any bugs are mine. I've also rearranged the code for timeouts in order to avoid interfering with the shutdown poll. git-svn-id: http://svn.python.org/projects/python/trunk@61289 6015fed2-1504-0410-9fe1-9d1591cc4771
1 parent f555420 commit bd0bb00

File tree

4 files changed

+118
-86
lines changed

4 files changed

+118
-86
lines changed

Doc/library/socketserver.rst

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ or inappropriate for the service) is to maintain an explicit table of partially
113113
finished requests and to use :func:`select` to decide which request to work on
114114
next (or whether to handle a new incoming request). This is particularly
115115
important for stream services where each client can potentially be connected for
116-
a long time (if threads or subprocesses cannot be used).
116+
a long time (if threads or subprocesses cannot be used). See :mod:`asyncore` for
117+
another way to manage this.
117118

118119
.. XXX should data and methods be intermingled, or separate?
119120
how should the distinction between class and instance variables be drawn?
@@ -132,16 +133,24 @@ Server Objects
132133

133134
.. function:: handle_request()
134135

135-
Process a single request. This function calls the following methods in order:
136-
:meth:`get_request`, :meth:`verify_request`, and :meth:`process_request`. If
137-
the user-provided :meth:`handle` method of the handler class raises an
138-
exception, the server's :meth:`handle_error` method will be called.
136+
Process a single request. This function calls the following methods in
137+
order: :meth:`get_request`, :meth:`verify_request`, and
138+
:meth:`process_request`. If the user-provided :meth:`handle` method of the
139+
handler class raises an exception, the server's :meth:`handle_error` method
140+
will be called. If no request is received within :attr:`self.timeout`
141+
seconds, :meth:`handle_timeout` will be called and :meth:`handle_request`
142+
will return.
139143

140144

141-
.. function:: serve_forever()
145+
.. function:: serve_forever(poll_interval=0.5)
142146

143-
Handle an infinite number of requests. This simply calls :meth:`handle_request`
144-
inside an infinite loop.
147+
Handle requests until an explicit :meth:`shutdown` request. Polls for
148+
shutdown every *poll_interval* seconds.
149+
150+
151+
.. function:: shutdown()
152+
153+
Tells the :meth:`serve_forever` loop to stop and waits until it does.
145154

146155

147156
.. data:: address_family
@@ -195,10 +204,9 @@ The server classes support the following class variables:
195204

196205
.. data:: timeout
197206

198-
Timeout duration, measured in seconds, or :const:`None` if no timeout is desired.
199-
If no incoming requests are received within the timeout period,
200-
the :meth:`handle_timeout` method is called and then the server resumes waiting for
201-
requests.
207+
Timeout duration, measured in seconds, or :const:`None` if no timeout is
208+
desired. If :meth:`handle_request` receives no incoming requests within the
209+
timeout period, the :meth:`handle_timeout` method is called.
202210

203211
There are various server methods that can be overridden by subclasses of base
204212
server classes like :class:`TCPServer`; these methods aren't useful to external

Lib/SocketServer.py

Lines changed: 66 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,13 @@ class will essentially render the service "deaf" while one request is
130130

131131

132132
import socket
133+
import select
133134
import sys
134135
import os
136+
try:
137+
import threading
138+
except ImportError:
139+
import dummy_threading as threading
135140

136141
__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer",
137142
"ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler",
@@ -149,7 +154,8 @@ class BaseServer:
149154
Methods for the caller:
150155
151156
- __init__(server_address, RequestHandlerClass)
152-
- serve_forever()
157+
- serve_forever(poll_interval=0.5)
158+
- shutdown()
153159
- handle_request() # if you do not use serve_forever()
154160
- fileno() -> int # for select()
155161
@@ -190,6 +196,8 @@ def __init__(self, server_address, RequestHandlerClass):
190196
"""Constructor. May be extended, do not override."""
191197
self.server_address = server_address
192198
self.RequestHandlerClass = RequestHandlerClass
199+
self.__is_shut_down = threading.Event()
200+
self.__serving = False
193201

194202
def server_activate(self):
195203
"""Called by constructor to activate the server.
@@ -199,27 +207,73 @@ def server_activate(self):
199207
"""
200208
pass
201209

202-
def serve_forever(self):
203-
"""Handle one request at a time until doomsday."""
204-
while 1:
205-
self.handle_request()
210+
def serve_forever(self, poll_interval=0.5):
211+
"""Handle one request at a time until shutdown.
212+
213+
Polls for shutdown every poll_interval seconds. Ignores
214+
self.timeout. If you need to do periodic tasks, do them in
215+
another thread.
216+
"""
217+
self.__serving = True
218+
self.__is_shut_down.clear()
219+
while self.__serving:
220+
# XXX: Consider using another file descriptor or
221+
# connecting to the socket to wake this up instead of
222+
# polling. Polling reduces our responsiveness to a
223+
# shutdown request and wastes cpu at all other times.
224+
r, w, e = select.select([self], [], [], poll_interval)
225+
if r:
226+
self._handle_request_noblock()
227+
self.__is_shut_down.set()
228+
229+
def shutdown(self):
230+
"""Stops the serve_forever loop.
231+
232+
Blocks until the loop has finished. This must be called while
233+
serve_forever() is running in another thread, or it will
234+
deadlock.
235+
"""
236+
self.__serving = False
237+
self.__is_shut_down.wait()
206238

207239
# The distinction between handling, getting, processing and
208240
# finishing a request is fairly arbitrary. Remember:
209241
#
210242
# - handle_request() is the top-level call. It calls
211-
# await_request(), verify_request() and process_request()
212-
# - get_request(), called by await_request(), is different for
213-
# stream or datagram sockets
243+
# select, get_request(), verify_request() and process_request()
244+
# - get_request() is different for stream or datagram sockets
214245
# - process_request() is the place that may fork a new process
215246
# or create a new thread to finish the request
216247
# - finish_request() instantiates the request handler class;
217248
# this constructor will handle the request all by itself
218249

219250
def handle_request(self):
220-
"""Handle one request, possibly blocking."""
251+
"""Handle one request, possibly blocking.
252+
253+
Respects self.timeout.
254+
"""
255+
# Support people who used socket.settimeout() to escape
256+
# handle_request before self.timeout was available.
257+
timeout = self.socket.gettimeout()
258+
if timeout is None:
259+
timeout = self.timeout
260+
elif self.timeout is not None:
261+
timeout = min(timeout, self.timeout)
262+
fd_sets = select.select([self], [], [], timeout)
263+
if not fd_sets[0]:
264+
self.handle_timeout()
265+
return
266+
self._handle_request_noblock()
267+
268+
def _handle_request_noblock(self):
269+
"""Handle one request, without blocking.
270+
271+
I assume that select.select has returned that the socket is
272+
readable before this function was called, so there should be
273+
no risk of blocking in get_request().
274+
"""
221275
try:
222-
request, client_address = self.await_request()
276+
request, client_address = self.get_request()
223277
except socket.error:
224278
return
225279
if self.verify_request(request, client_address):
@@ -229,21 +283,6 @@ def handle_request(self):
229283
self.handle_error(request, client_address)
230284
self.close_request(request)
231285

232-
def await_request(self):
233-
"""Call get_request or handle_timeout, observing self.timeout.
234-
235-
Returns value from get_request() or raises socket.timeout exception if
236-
timeout was exceeded.
237-
"""
238-
if self.timeout is not None:
239-
# If timeout == 0, you're responsible for your own fd magic.
240-
import select
241-
fd_sets = select.select([self], [], [], self.timeout)
242-
if not fd_sets[0]:
243-
self.handle_timeout()
244-
raise socket.timeout("Listening timed out")
245-
return self.get_request()
246-
247286
def handle_timeout(self):
248287
"""Called if no new request arrives within self.timeout.
249288
@@ -307,7 +346,8 @@ class TCPServer(BaseServer):
307346
Methods for the caller:
308347
309348
- __init__(server_address, RequestHandlerClass, bind_and_activate=True)
310-
- serve_forever()
349+
- serve_forever(poll_interval=0.5)
350+
- shutdown()
311351
- handle_request() # if you don't use serve_forever()
312352
- fileno() -> int # for select()
313353
@@ -523,7 +563,6 @@ def process_request_thread(self, request, client_address):
523563

524564
def process_request(self, request, client_address):
525565
"""Start a new thread to process the request."""
526-
import threading
527566
t = threading.Thread(target = self.process_request_thread,
528567
args = (request, client_address))
529568
if self.daemon_threads:

Lib/test/test_socketserver.py

Lines changed: 29 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
test.test_support.requires("network")
2323

24-
NREQ = 3
2524
TEST_STR = "hello world\n"
2625
HOST = "localhost"
2726

@@ -50,43 +49,6 @@ class ForkingUnixDatagramServer(SocketServer.ForkingMixIn,
5049
pass
5150

5251

53-
class MyMixinServer:
54-
def serve_a_few(self):
55-
for i in range(NREQ):
56-
self.handle_request()
57-
58-
def handle_error(self, request, client_address):
59-
self.close_request(request)
60-
self.server_close()
61-
raise
62-
63-
64-
class ServerThread(threading.Thread):
65-
def __init__(self, addr, svrcls, hdlrcls):
66-
threading.Thread.__init__(self)
67-
self.__addr = addr
68-
self.__svrcls = svrcls
69-
self.__hdlrcls = hdlrcls
70-
self.ready = threading.Event()
71-
72-
def run(self):
73-
class svrcls(MyMixinServer, self.__svrcls):
74-
pass
75-
if verbose: print "thread: creating server"
76-
svr = svrcls(self.__addr, self.__hdlrcls)
77-
# We had the OS pick a port, so pull the real address out of
78-
# the server.
79-
self.addr = svr.server_address
80-
self.port = self.addr[1]
81-
if self.addr != svr.socket.getsockname():
82-
raise RuntimeError('server_address was %s, expected %s' %
83-
(self.addr, svr.socket.getsockname()))
84-
self.ready.set()
85-
if verbose: print "thread: serving three times"
86-
svr.serve_a_few()
87-
if verbose: print "thread: done"
88-
89-
9052
@contextlib.contextmanager
9153
def simple_subprocess(testcase):
9254
pid = os.fork()
@@ -143,28 +105,48 @@ def pickaddr(self, proto):
143105
self.test_files.append(fn)
144106
return fn
145107

146-
def run_server(self, svrcls, hdlrbase, testfunc):
108+
def make_server(self, addr, svrcls, hdlrbase):
109+
class MyServer(svrcls):
110+
def handle_error(self, request, client_address):
111+
self.close_request(request)
112+
self.server_close()
113+
raise
114+
147115
class MyHandler(hdlrbase):
148116
def handle(self):
149117
line = self.rfile.readline()
150118
self.wfile.write(line)
151119

152-
addr = self.pickaddr(svrcls.address_family)
120+
if verbose: print "creating server"
121+
server = MyServer(addr, MyHandler)
122+
self.assertEquals(server.server_address, server.socket.getsockname())
123+
return server
124+
125+
def run_server(self, svrcls, hdlrbase, testfunc):
126+
server = self.make_server(self.pickaddr(svrcls.address_family),
127+
svrcls, hdlrbase)
128+
# We had the OS pick a port, so pull the real address out of
129+
# the server.
130+
addr = server.server_address
153131
if verbose:
132+
print "server created"
154133
print "ADDR =", addr
155134
print "CLASS =", svrcls
156-
t = ServerThread(addr, svrcls, MyHandler)
157-
if verbose: print "server created"
135+
t = threading.Thread(
136+
name='%s serving' % svrcls,
137+
target=server.serve_forever,
138+
# Short poll interval to make the test finish quickly.
139+
# Time between requests is short enough that we won't wake
140+
# up spuriously too many times.
141+
kwargs={'poll_interval':0.01})
142+
t.setDaemon(True) # In case this function raises.
158143
t.start()
159144
if verbose: print "server running"
160-
t.ready.wait(10)
161-
self.assert_(t.ready.isSet(),
162-
"%s not ready within a reasonable time" % svrcls)
163-
addr = t.addr
164-
for i in range(NREQ):
145+
for i in range(3):
165146
if verbose: print "test client", i
166147
testfunc(svrcls.address_family, addr)
167148
if verbose: print "waiting for server"
149+
server.shutdown()
168150
t.join()
169151
if verbose: print "done"
170152

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ Core and builtins
1818
Library
1919
-------
2020

21+
- Issue #1193577: A .shutdown() method has been added to SocketServers
22+
which terminates the .serve_forever() loop.
23+
2124
- Bug #2220: handle rlcompleter attribute match failure more gracefully.
2225

2326
- Issue #2225: py_compile, when executed as a script, now returns a non-

0 commit comments

Comments
 (0)
X Tutup