Swirl: some sugar for Tornado

Tornado is a non-blocking server and Web framework from Facebook. One of the nice features of Tornado is its ability to respond to requests asynchronously. The Tornado tutorial includes this example of a request handler that builds its results by calling on the FriendFeed API:

class MainHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        http.fetch("http://friendfeed-api.com/v2/feed/bret",
                   callback=self.async_callback(self.on_response))

    def on_response(self, response):
        if response.error: raise tornado.web.HTTPError(500)
        json = tornado.escape.json_decode(response.body)
        self.write("Fetched %d entries from the FriendFeed API" %
            len(json['entries']))
        self.finish()

The tornado.web.asynchronous decorator on get instructs Tornado to not automatically close the response when get returns. Tornado's asynchronous HTTP client automatically calls on_response when a response has been received from FriendFeed, and that method fills in the response to the browser, and manually finishes it.

It works, but this approach has some warts. The need to use an explicit callback function breaks up the flow of your code, your callbacks must manually check for errors, and you have to remember to call the finish method, or the user's browser will hang indefinitely waiting for the request to complete.

Swirl uses Python's support for coroutines to remove these warts, letting you write the above request handler much like you would write it with a synchronous HTTP request.

Coroutines?

Normal functions in Python have a single entry point, and stop executing as soon as they return a value. Callers pass in some arguments, the function executes until it reaches a return statement, and the function exits.

But it doesn't always have to be this way. Python includes a very handy mechanism for creating sequences: the yield statement. When a function uses yield, the function is able to "return" as many values as it wants. A simple example is a function that iteratively returns the squares of all numbers from one to n.

def squares(n):
    for i in range(1, n):
        yield i * i

for s in squares(5):
    print s,
# outputs: 1 4 9 16

We can use a for loop to iterate over these values, just as if squares had returned a list of them.

What's so neat about yield is that it transfers program flow in and out of our squares function. We start in the first line of the for loop, and enter squares until it yields a value, and then return the body of the loop. We go back into squares to see if it has another value, and if it does, flow returns to the loop to print the new value.

This transfer of flow is exactly what needs to happen to generate an asynchronous response in Tornado. Tornado calls a request handler, which starts some task whose results it needs to generate the response. Flow returns to Tornado's I/O loop, which executes the task, and then calls (another part of) the request handler to complete the response.

We can use Python's yield statement to implement this transfer of flow more elegantly than Tornado does.

Swirl's Solution

Here is the Swirl version of the FriendFeed API example:

import swirl

class MainHandler(tornado.web.RequestHandler):
    @swirl.asynchronous
    def get(self):
        http = tornado.httpclient.AsyncHTTPClient()
        uri = 'http://friendfeed-api.com/v2/feed/bret'
        
        try:
            response = yield lambda cb: http.fetch(uri, cb)
            json = tornado.escape.json_decode(response.body)
            self.write('Fetched %d entries from the FriendFeed API.' %
                len(json['entries']))
        except tornado.httpclient.HTTPError:
            raise tornado.web.HTTPError(500, 'API request failed')

The strangest-looking part of this code is the first line after the try. What's happening there?

  1. We use lambda to create an anonymous function which executes the work that we want done in the background on Tornado's I/O loop. (We'll refer to this function as the "worker function".)
  2. We yield the worker function. By yielding, we suspend execution of our handler and pass the worker function to Swirl.
  3. Swirl creates a function to serve as the callback for our task. It calls our worker function, passing in this callback as the cb parameter. The worker function calls http.fetch, passing it the Swirl callback function and starting the HTTP request.
  4. Swirl returns control to the Tornado I/O loop, letting it continue to handle incoming requests and do other tasks (like executing our FriendFeed API request).
  5. When the FriendFeed API request finishes, the asynchronous HTTP client calls Swirl's callback function. Swirl interprets the values that were passed to this function, and passes it into our request handler. The yield expression returns these values, which get assigned to the response variable, and the handler proceeds to interpret the response, count the JSON entries, and send its own response.

With just a little bit of yield and lambda boilerplate, we have eliminated all of the trickery of writing an asynchronous request handler in Tornado. The response is implicitly finished when get reaches its end and stops yielding tasks, so there's no need to explicitly call self.finish(). We no longer have to split our handler into two functions, and we can handle API errors the normal way: using a try/except block.

Callback Argument Handling

Swirl automatically supports two ways for asynchronous tasks to indicate exceptions to callbacks.

When an exception is detected using one of these methods, Swirl raises it at the point of the yield statement that yielded the task that caused it. This exception (as shown in the above example) can be caught using a standard except block.

When no exception is detected, Swirl returns the tuple of the arguments passed to the callback from the yield statement. If the last argument is None, this is treated as an empty error argument, and it is omitted. If the callback was only passed one non-error argument the tuple is forgone and the argument value is returned directly.

I/O Loops

The swirl.asynchronous decorator runs worker functions on Tornado's default I/O loop (the one accessible from ioloop.IOLoop.instance()). If your application is using a different loop, you can create a decorator function that runs workers on your loop by calling swirl.make_asynchronous_decorator:

from swirl import make_asynchronous_decorator

my_loop = tornado.ioloop.IOLoop()
asynchronous = make_asynchronous_decorator(my_loop)

# use asynchronous as a decorator, as in the above FriendFeed example

Get Swirl

The latest version of Swirl is 0.1.1; it requires Python ≥ 2.5 and either Tornado 0.2 or the latest version from GitHub. You can install Swirl in one of two ways:

Swirl is distributed under the terms of the MIT license.

Contribute to Swirl

Swirl is an open-source project hosted on GitHub. You are free to browse or clone its repository, and to fork the code and make any modifications you wish. Useful modifications can be incorporated into the Swirl mainline to appear in future releases.

If you have any suggestions for Swirl, or encounter any bugs, please note them on the Swirl issue tracker.