About | Home > ComputerStuff > Software > Programming > ProgrammingLanguages > Python > HowBottleAppsWork | Contact
About | Home > ComputerStuff > Software > Programming > Frameworks > WebFrameworks > HowBottleAppsWork | Contact
Last edited on May 14th 2015
TL;DR : it is possible to have multihreaded or multiprocess bottle app using only the standard python library
This is a story about a web application I wrote some time ago which didn't work appropriately when more than one person was using it.
In the beginning, I wrote my app using only the standard library modules, namely the classes available in SocketServer.py and BaseHTTPServer.py. By doing so, I found that my application was running well, but only if I was the unique user. As soon as there was more than one user, problems seem to appear at random moments : our browsers would display the usual spinners, seeming to be loading the page forever, and then eventually timeout because the server didn't care to respond.
I had no idea why was that happening, so I started seeking for help on CLP(here and here), reddit, IRC, and everyone told me not to use the standard python 2 library because it doesn't work : it's monothreaded (I figured out later that this is wrong) and single-processed (also wrong) and advised to use a real webframework instead.
When I first wrote the app, I didn't consider this option becasue it was really really simple (no database, no user login, no session no nothing...). It's just a monitoring app that displays the status of our audio streaming infrastructure by querying some webservices. Eventually, I listend to the offered advices and finally took time to consider what options were offered to me. I started looking at what seemed cool or new to me (namely CherryPy, Bottle.py, Circuits, Quixote and Weblayer), and finally picked bottle.py because of its minimalism. A true microframework (~3000 lines).
But even after rewriting the app in bottle.py, it behaved similarly to the first : as soon as there was more than one user, the application would hang up indifinetly until I restarted it. I could see that it was still running, but it was not responding to any further HTTP requests from any browsers.
Where was the program hanging ?
How was I supposed to find where the problem is ? this was not the usual debugging drill : I had no errors, no exceptions, nothing printed in the console or logfile. In fact, I wasn't looking for errors to begin with ! I was looking for something that didn't happen (an http request timeout). It's hard to look for things that don't happen ! and then I thought what If I launched the program from the console, in the foreground, then interrupt it (Ctrl-c) ? it should print a stacktrace of what instruction it was currently executing. So I tried that, and here's what I got:
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ python app.py Bottle v0.13-dev server starting up (using WSGIRefServer())... Listening on http://localhost:8010/ Hit Ctrl-C to quit. ^C---------------------------------------- Exception happened during processing of request from ('127.0.0.1', 39537) Traceback (most recent call last): File "/usr/lib/python2.7/SocketServer.py", line 295, in _handle_request_noblock self.process_request(request, client_address) File "/usr/lib/python2.7/SocketServer.py", line 321, in process_request self.finish_request(request, client_address) File "/usr/lib/python2.7/SocketServer.py", line 334, in finish_request self.RequestHandlerClass(request, client_address, self) File "/usr/lib/python2.7/SocketServer.py", line 651, in __init__ self.handle() File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116, in handle self.raw_requestline = self.rfile.readline() File "/usr/lib/python2.7/socket.py", line 447, in readline data = self._sock.recv(self._rbufsize) KeyboardInterrupt ---------------------------------------- 127.0.0.1 - - [20/Apr/2015 10:28:05] "GET / HTTP/1.1" 200 12
I could see there that the problem was in self._sock.recv(self._rbufsize), so it was clearely a problem in sockets. I also found supporting evidence with strace :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ pgrep -a python 5420 python app.py ychaouche-PC HANGINGBOTTLE # strace -p 5420 Process 5420 attached recvfrom(4,
recvfrom is a system call that receives data from a socket. It's a blocking call, that means that the execution of the program will stop until the function returns.
I wasn't sure though that the printed stack trace was really representative of where the program was really hanging, so I added another layer of verification with faulthandler (https://pypi.python.org/pypi/faulthandler/), written by Victor "haypo" Stinner (http://haypo-notes.readthedocs.org/), just to compare the results and be sure
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run File "app.py", line 11 in <module>
The content of this stack trace is very similar to the python traceback if you read it in reverse (most recent call is printed first instead of last), and if you strip out the code lines.
Both traces show that the last line of code that was executed was line 447 in socket.py, which is, again :
data = self._sock.recv(self._rbufsize)
Why are the requests that are taking too long not timing out ? And why can't my server process another request when one of them isn't finished anyway ?
So here we have two problems to solve :
- We need to make sure that our sockets timeout, so that the server can drop them and move on instead of wasting resources for no reason (polling)
- we also need to make the server process many requests in parallel, not just one, so that even if a request takes a little time to be processed, further requests won't wait for it to finish before being handled.
What happened in my case (most likely) is that one client opened a connection to my server but didn't send any requests, or, it was too slow to receive the response. So my server was waiting for the request there, and chose to ignore all further requests, either from the same client on different sockets of from different clients, until it had finished with that first request. We don't want this. We want each incoming request to be treated, either in a separate thread or in a seperate process, as soon as it reaches the server.
Also, if a request is taking too long and the client is not responding, we want to terminate by triggering a timeout, so that we can move on and avoid wasting ressources.
First, let's make the sockets timeout
So in order to demonstrate my problem, I had to find a way to simulate a browser opening a connection and never sending anything, so that the server would hang in self._sock.recv(self._rbufsize) in socket.py line 447, like earlier, by writing a little python script. I would start my server, start my fake client, then in order to prove the server is unresponsive I would just use curl to download the "/" page.
Here's the client code :
# client.py # opens a connection to my server and never sends it anything. # leaving the connection open until killing it from the command line (^C) import socket import requests # This will leave an open a connection to our app. conn = socket.create_connection(('localhost',8010)) # This will never get anything until conn.close() is called. print requests.get('http://localhost:8010').text.strip()
Here's the server's code, we'll use a very simple web hello world app here :
# app.py # contains the web application. import bottle import faulthandler import signal @bottle.route("/") def main(): return "Hello there\n" faulthandler.register(signal.SIGUSR1) bottle.run(host='localhost',port=8010)
Try and run the app.py server, then the client.py client, then curl http://localhost:8010. curl won't obtain any response from the server until client.py is killed. Here's a video showing it in action http://youtu.be/SOKLZBY5iaE
How to make sockets timeout ?
We need to find where is the socket we need to put a timeout on defined in the source code. For that, let's reeaxmine the stack trace we had earlier, by using faulthandler, to figure out what creates that socket :
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline ->File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle<- File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run File "app.py", line 11 in <module>
The highlighted line is the second-to-last line of code that was executed before the application hung up. Here's the source code showing in which method and class was that happening :
# This is from wsgiref/simple_server.py, around line 69 class WSGIRequestHandler(BaseHTTPRequestHandler): [...] def handle(self): """Handle a single HTTP request""" #-----> self.raw_requestline = self.rfile.readline() # <------- line 116 # where is this self.rfile defined ? and how can I # define a timeout on it (if it's a socket) ? if not self.parse_request(): # An error code has been sent, just exit return handler = ServerHandler( self.rfile, self.wfile, self.get_stderr(), self.get_environ() ) handler.request_handler = self # backpointer for logging handler.run(self.server.get_app())
I had to search for this self.rfile variable and see if setting a timeout on it (it seemed to behave just a like a socket) would solve my problems. In doing so, I had the idea of climbing up the classes hierarchy of WSGIRequestHandler, the class where the code was hanging, in search of that attribute (self.rfile).
The StreamRequestHandler base class (shown in blue) is the one that defines the self.rfile and self.wfile attributes which are used in the handle method (which executes the hanging line self.raw_requestline = self.rfile.readline()), and defines timeouts on them in its setup method like this :
# From SocketServer.py, around line 675 class StreamRequestHandler(BaseRequestHandler): """Define self.rfile and self.wfile for stream sockets.""" [...] # A timeout to apply to the request socket, if not None. timeout = None #<---- Interresting ! [...] def setup(self): self.connection = self.request if self.timeout is not None: self.connection.settimeout(self.timeout) if self.disable_nagle_algorithm: self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) self.rfile = self.connection.makefile('rb', self.rbufsize) self.wfile = self.connection.makefile('wb', self.wbufsize)
In which inheriting class should I set that timeout ?
So rfile and wfile are products of a socket, because self.connection itself is a socket and a self.connection.makefile, according to its docstring, will merely create a regular file object corresponding to the socket, to make the usual file methods available (read, write, seek, tell etc.)
All I had to do, it seems, is to define the timeout in the inherited class used by bottle.py, or define my own, which will only have one line of code :
class MyHandler([ClassUsedByBottle]): timeout = 30 # sets a timeout of 30 seconds.
and find a way to ask bottle.py to use that class instead.
Which class is used by bottle.py ?
We already saw that the timeout isn't defined in bottle.py's class itself, but one of its ancestors, StreamRequestHandler defined in SocketServer.py. Our precious traceback shows what are the different methods that were called during the processing of our request. This can help us find what class is bottle.py using :
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever ->File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run<- File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run File "app.py", line 11 in <module>
In particular, the run method at line 2684 shows interresting content :
# From bottle.py version 0.13-dev, around line 2655 class WSGIRefServer(ServerAdapter): def run(self, app): # pragma: no cover from wsgiref.simple_server import make_server from wsgiref.simple_server import WSGIRequestHandler, WSGIServer import socket class FixedHandler(WSGIRequestHandler): def address_string(self): # Prevent reverse DNS lookups please. return self.client_address[0] def log_request(*args, **kw): if not self.quiet: return WSGIRequestHandler.log_request(*args, **kw) handler_cls = self.options.get('handler_class', FixedHandler) server_cls = self.options.get('server_class', WSGIServer) [...] ----->self.srv = make_server(self.host, self.port, app, server_cls, handler_cls)<-------- self.port = self.srv.server_port # update port actual port (0 means random) try: --------->self.srv.serve_forever()<------------- except KeyboardInterrupt: self.srv.server_close() # Prevent ResourceWarning: unclosed socket raise
Basically, this function will create an HTTP server with
self.srv = make_server(self.host, self.port, app, server_cls, handler_cls)
then run it with
self.srv.serve_forever()
If you read the source code of the make_server function and follow on to the base classes, you will find that python 2 standard library's HTTP servers need to be passed a RequestHandler. RequestHandler is a base class defined in SocketServer.py. bottle.py defines it's own request handler class, the FixedHandler, which is a subclass of WSGIRequestHandler. We already have the class hierarchy of WSGIRequestHandler above, we can now updated it with our final class (used by bottle.py), FixedHandler :
There's a paritucular line of code in that run method above that shows that it is likely possible to pass on our own RequestHandler class instead of FixedHandler :
# from bottle.py, around line 2655 class WSGIRefServer(ServerAdapter): def run(self, app): # pragma: no cover [...] ------> handler_cls = self.options.get('handler_class', FixedHandler) <--------- # server_cls = self.options.get('server_class', WSGIForkServer) server_cls = self.options.get('server_class', WSGIServer) [...]
It seems I just have to pass in a handler_class option. But where is the self.options attribute defined ? how can I add options to it ? and even if I was able to do so, how am I to use the FixedHandeler in my own source code when it is is defined inside WSGIRefServer's run method ? this is making it inaccessible from the outside code. If it was defined at the class level, I could just do
import bottle import WSGIRefHandler class MyRequestHandler(WSGIRefHandler.FixedHandler): timeout = 30
But since it's inside a function, I can not do that. As an alternative, I simply patched the FixedHandler class instead, directly in the source code of bottle.py (I know, it's bad...) :
# from bottle.py, around line 2655 class WSGIRefServer(ServerAdapter): def run(self, app): # pragma: no cover [...] class FixedHandler(WSGIRequestHandler): ---------> timeout = 5 # sets a timeout of 5 seconds, just for test purposes. <------------ # otherwise I would have set 30 def address_string(self): # Prevent reverse DNS lookups please. return self.client_address[0] def log_request(*args, **kw): if not self.quiet: return WSGIRequestHandler.log_request(*args, **kw)
Testing the new timing out FixedHandler class
I tried my app again, with the faulty client.py test script :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ python app.py Bottle v0.13-dev server starting up (using WSGIRefServer())... Listening on http://localhost:8010/ Hit Ctrl-C to quit. [...]
I let the server run in the foreground and start the client in another terminal
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ time python client.py [...]
Notice that I am using the time command here, to see how much time the client.py script runs before the server terminates the connection on timeout. After 5 seconds, here's what I got in the client's console :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ time python client.py [... after 5 seconds ...] Hello there real 0m5.083s user 0m0.067s sys 0m0.017s ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $
and here's what I got on the server's console :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ python app.py Bottle v0.13-dev server starting up (using WSGIRefServer())... Listening on http://localhost:8010/ Hit Ctrl-C to quit. [... after 5 seconds ...] ---------------------------------------- Exception happened during processing of request from ('127.0.0.1', 56756) Traceback (most recent call last): File "/usr/lib/python2.7/SocketServer.py", line 295, in _handle_request_noblock self.process_request(request, client_address) File "/usr/lib/python2.7/SocketServer.py", line 321, in process_request self.finish_request(request, client_address) File "/usr/lib/python2.7/SocketServer.py", line 334, in finish_request self.RequestHandlerClass(request, client_address, self) File "/usr/lib/python2.7/SocketServer.py", line 651, in __init__ self.handle() File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116, in handle self.raw_requestline = self.rfile.readline() File "/usr/lib/python2.7/socket.py", line 447, in readline data = self._sock.recv(self._rbufsize) timeout: timed out <---------------- ---------------------------------------- 127.0.0.1 - - [21/Apr/2015 14:37:15] "GET / HTTP/1.1" 200 12
Now I had to tackle the second problem : handle requests in parallel
Parallel requests processing
First, we need to understand how requests are processed. If we play back our traceback, we can understand how bottle.py works in the inside, how it recieves requests, and how it deals with them.
The starting point of a bottle.py application is the bottle.run function, as our precious traceback showed :
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run ->File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run<- File "app.py", line 11 in <module>
Based on this stack trace, I made this sequence diagram showing what happens when a request comes in :
Reading the docstring of the SocketServer.py module, the standard library's module that defines the base class of WSGIServer, the default server used by bottle.py, I discovered that it is actually possible to inherit from a mixin class to make our servers multi-threaded or multi-processed :
# From SocketServer.py, at the beginning of the file """ This module tries to capture the various aspects of defining a server: For socket-based servers: [...] For request-based servers (including socket-based): [...] - how to handle multiple requests: - synchronous (one request is handled at a time) - forking (each request is handled by a new process) - threading (each request is handled by a new thread) The classes in this module favor the server type that is simplest to write: a synchronous TCP/IP server. This is bad class design, but save some typing. (There's also the issue that a deep class hierarchy slows down method lookups.) """
Then it says :
""" Forking and threading versions of each type of server can be created using the ForkingMixIn and ThreadingMixIn mix-in classes. [...] The Mix-in class must come first, since it overrides a metho defined [...] """
So according to this, one has only to add either ForkingMixIn or ThreadingMixIn to the base classes of the server he choses to use with bottle.py. In our case, we didn't specify any server, so bottle.py picked up WSGIServer. This is visible in the run function of bottle.py :
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run ->File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run<- File "app.py", line 11 in <module>
# This is from bottle.py version 0.13-dev, around line 2972 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,...): """ [...The documentation says here that server is the kind of server to use. It can be a string or an instance of ServerAdapter, a class they provide. You can pass a string to use a variety of supported servers like CherryPy, Waitress (pyramid), Paste etc.] :param server: Server adapter to use. See :data:`server_names` keys for valid names or pass a :class:`ServerAdapter` subclass. (default: `wsgiref`) """ try: [...] if server in server_names: ----------->server = server_names.get(server)<--------------- if isinstance(server, basestring): server = load(server) if isinstance(server, type): server = server(host=host, port=port, **kargs) if not isinstance(server, ServerAdapter): raise ValueError("Unknown or unsupported server: %r" % server) [...] if reloader: lockfile = os.environ.get('BOTTLE_LOCKFILE') bgcheck = FileCheckerThread(lockfile, interval) with bgcheck: --------------->server.run(app)<-------------- if bgcheck.status == 'reload': sys.exit(3) else: ----------->server.run(app)<------------ [...]
So by default, the server argument of run is the string 'wsgiref' (checkout run's default arguments), and the line
server = server_names.get(server)
will select the appropriate class for us, which is WSGIRefServer:
# This is in bottle.py around line 2905 server_names = { 'cgi': CGIServer, 'flup': FlupFCGIServer, --->'wsgiref': WSGIRefServer,<--- 'waitress': WaitressServer, 'cherrypy': CherryPyServer, [...] }
Now back to our sequence diagram :
We will create a new server class, that will inherit from both WSGIServer and ForkingMixIn (we could also choose ThreadingMixIn just as well), and find a way to have that being the server used by bottle.py.
Still based on the stack trace, we can see that the WSGIServer is selected in bottle.py :: WSGIRefServer :: run :
Current thread 0x00007ff53dac5740 (most recent call first): File "/usr/lib/python2.7/socket.py", line 447 in readline File "/usr/lib/python2.7/wsgiref/simple_server.py", line 116 in handle File "/usr/lib/python2.7/SocketServer.py", line 651 in __init__ File "/usr/lib/python2.7/SocketServer.py", line 334 in finish_request File "/usr/lib/python2.7/SocketServer.py", line 321 in process_request File "/usr/lib/python2.7/SocketServer.py", line 295 in _handle_request_noblock File "/usr/lib/python2.7/SocketServer.py", line 238 in serve_forever ->File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 2684 in run<- File "/home/ychaouche/CODE/TEST/HANGINGBOTTLE/bottle.py", line 3052 in run File "app.py", line 11 in <module>
# from bottle.py, around line 2655 class WSGIRefServer(ServerAdapter): def run(self, app): # pragma: no cover [...] handler_cls = self.options.get('handler_class', FixedHandler) ------->server_cls = self.options.get('server_class', WSGIServer)<---------- [...] self.srv = make_server(self.host, self.port, app, server_cls, handler_cls) self.port = self.srv.server_port # update port actual port (0 means random) try: ----------->self.srv.serve_forever()<-------------- except KeyboardInterrupt: self.srv.server_close() # Prevent ResourceWarning: unclosed socket raise
In the first highlighted line, we see that server_cls is actually read from options, and in the case of server_cls not being defined, it would default to WSGIServer. So our job is to supply our new class in this options attribute. self.options is actually being defined in bottle.py :: ServerAdapter
# from bottle.py around line 2623 class ServerAdapter(object): quiet = False def __init__(self, host='127.0.0.1', port=8080, **options): ------->self.options = options<-------- self.host = host self.port = int(port) def run(self, handler): # pragma: no cover pass def __repr__(self): args = ', '.join(['%s=%s'%(k,repr(v)) for k, v in self.options.items()]) return "%s(%s)" % (self.__class__.__name__, args)
ServerAdapter is the parent of WSGIRefServer (look at previous source code). So the options here are just any keyword argument you pass to the ServerAdapter or its subclasses (WSGIRefServer in our case). The highlighted line below shows that if you pass a ServerAdapter instance to bottle.py's run function, then all the keyword arguments you pass to it will be set to the self.options attribute that we saw earlier :
# This is from bottle.py version 0.13-dev, around line 2972 def run(app=None, server='wsgiref', host='127.0.0.1', port=8080,...): [...] try: [...] if server in server_names: server = server_names.get(server) if isinstance(server, basestring): server = load(server) if isinstance(server, type): ----------->server = server(host=host, port=port, **kargs)<-------------- if not isinstance(server, ServerAdapter): raise ValueError("Unknown or unsupported server: %r" % server) [...] if reloader: lockfile = os.environ.get('BOTTLE_LOCKFILE') bgcheck = FileCheckerThread(lockfile, interval) with bgcheck: server.run(app) if bgcheck.status == 'reload': sys.exit(3) else: server.run(app) [...]
This means we can inject an option in bottle.py's run function and have it delivered to our WSGIRefServer class. Let's change the way I call bottle.run in my own application :
import bottle # The default server that bottle uses, monothreaded from wsgiref.simple_server import WSGIServer # The module where ForkedMixIn is defined import SocketServer import faulthandler import signal @bottle.route("/") def main(): return "Hello there\n" faulthandler.register(signal.SIGUSR1) # Let's define a new ForkServer class, which will have override the # default process_request method of WSGIServer with the one defined # in SocketServer.ForkingMixIn class ForkServer(SocketServer.ForkingMixIn,WSGIServer): pass # the "server_class" argument is different from the "server" argument ! # server remains the string "wsgiref" by default # server_class will be used inside WSGIRefServer.run bottle.run(host='localhost',port=8010,server_class=ForkServer)
Testing the new ForkServer
Now here's what happens when I start the server in one console :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ python app.py Bottle v0.13-dev server starting up (using WSGIRefServer(server_class=<class __main__.ForkServer at 0x7f39b8be1c18>))... Listening on http://localhost:8010/ Hit Ctrl-C to quit. [...]
Notice how the WSGIRefServer __repr__ string acknowledges that we're using a specific server_class (__main__.ForkServer), the one we defined and passed as server_class to bottle.run.
I launch the client in the other console :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ time python client.py Hello there real 0m0.092s user 0m0.072s sys 0m0.017s ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $
As you can see here in the client code, I have an immediate response from the server (no 5 seconds waiting) and a log line on the server :
ychaouche@ychaouche-PC ~/CODE/TEST/HANGINGBOTTLE $ python app.py Bottle v0.13-dev server starting up (using WSGIRefServer(server_class=<class __main__.ForkServer at 0x7f39b8be1c18>))... Listening on http://localhost:8010/ Hit Ctrl-C to quit. 127.0.0.1 - - [21/Apr/2015 16:49:19] "GET / HTTP/1.1" 200 12
The htop command shows that a second python process is launched upon the arrival of a request. Here's before :
and here's during the request
Mission accomplished.
Final considerations and caveats
This approach is only suitable for simple apps where no state-data is stored in memory. As the SocketServer.py module docstring reminds it, If one chooses to use forks, then he must make sure that state-data is accessible through the forks. If he choses to use threads, then he needs to provide a lock mechanisme so that data remains consistent between concurrent thread reads and writes. For web applications where data is stored on disk or in a database, for example, it should be fairly safe to use.
""" # From SocketServer.py's module dosctring [...] Of course, you still have to use your head! For instance, it makes no sense to use a forking server if the service contains state in memory that can be modified by requests (since the modifications in the child process would never reach the initial state kept in the parent process and passed to each child). In this case, you can use a threading server, but you will probably have to use locks to avoid two requests that come in nearly simultaneous to apply conflicting changes to the server state. On the other hand, if you are building e.g. an HTTP server, where all data is stored externally (e.g. in the file system), a synchronous class will essentially render the service "deaf" while one request is being handled -- which may be for a very long time if a client is slow to read all the data it has requested. Here a threading or forking server is appropriate. """
Last edited on May 14th 2015
____________________________________________________________________________________________________
To comment or ask questions, you may prefere to Contact me directly, as I get no notification when you post a comment in here.
Si vous êtes dans ma région et que avez besoin d'aide pour installer linux, vous pouvez me contacter
Sinon, vous pouvez chercher un parrain dans votre région.
Comments:
Note: You must be logged in to add comments