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.
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.
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?
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".)
yield the worker function. By yielding, we suspend
execution of our handler and pass the worker function to Swirl.
cb parameter. The worker function calls
http.fetch, passing it the Swirl callback function and
starting the HTTP request.
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.
Swirl automatically supports two ways for asynchronous tasks to indicate exceptions to callbacks.
error attribute. (AsyncHTTPClient does this.)
Exception object as its last
argument if there was an error, or None if there was
no error.
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.
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
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:
easy_install swirl from the command line.
python setup.py install from
within the directory you extracted.
Swirl is distributed under the terms of the MIT license.
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.