The wikispot.org service will be shut down on April 1st. Please see this page for more information.

HowBottleAppsWork

InfoInfo
Search:    

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([WWW]here and [WWW]here), [WWW]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.

http://i.imgur.com/OElB5ie.gif

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 ([WWW]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 :

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 [WWW]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 [WWW]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).

http://i.imgur.com/9otSl2b.png

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 :

http://i.imgur.com/VNeh27E.png

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

http://zippy.gfycat.com/UnkemptUnequaledAkitainu.gif

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 :

http://i.imgur.com/KiUryOX.png

http://i.imgur.com/1YzCnhq.png

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 :

http://i.imgur.com/PG3CWCf.png

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 :

http://i.imgur.com/xDFjltx.png

and here's during the request

http://i.imgur.com/5uNlmY6.png

Mission accomplished.

http://i.imgur.com/9385o7U.gif

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.

[WWW]Je suis parrain Linux http://i.imgur.com/ui89F.png.

Si vous êtes dans ma région et que avez besoin d'aide pour installer linux, vous pouvez me contacter
Sinon, [WWW]vous pouvez chercher un parrain dans votre région.

Comments:

Note: You must be logged in to add comments

This is a Wiki Spot wiki. Wiki Spot is a 501(c)3 non-profit organization that helps communities collaborate via wikis.