Persistent caching with fire-and-forget updates
14 December 2011
4 comments
Python, Tornado
I just recently landed some patches on toocool that implements and interesting pattern that is seen more and more these days. I call it: Persistent caching with fire-and-forget updates
Basically, the implementation is this: You issue a request that requires information about a Twitter user: E.g. http://toocoolfor.me/following/chucknorris/vs/peterbe
The app looks into its MongoDB for information about the tweeter and if it can't find this user it goes onto the Twitter REST API and looks it up and saves the result in MongoDB.
The next time the same information is requested, and the data is available in the MongoDB it instead checks if the modify_date or more than an hour and if so, it sends a job to the message queue (Celery with Redis in my case) to perform an update on this tweeter.
You can basically see the code here but just to reiterate and abbreviate, it looks like this:
tweeter = self.db.Tweeter.find_one({'username': username})
if not tweeter:
result = yield tornado.gen.Task(...)
if result:
tweeter = self.save_tweeter_user(result)
else:
# deal with the error!
elif age(tweeter['modify_date']) > 3600:
tasks.refresh_user_info.delay(username, ...)
# render the template!
What the client gets, i.e. the user using the site, is it that apart from the very first time that URL is request is instant results but data is being maintained and refreshed.
This pattern works great for data that doesn't have to be up-to-date to the second but that still needs a way to cache invalidate and re-fetch. This works because my limit of 1 hour is quite arbitrary. An alternative implementation would be something like this:
tweeter = self.db.Tweeter.find_one({'username': username})
if not tweeter or (tweeter and age(tweeter) > 3600 * 24 * 7):
# re-fetch from Twitter REST API
elif age(tweeter) > 3600:
# fire-and-forget update
That way you don't suffer from persistently cached data that is too old.
Integrate BrowserID in a Tornado web app
22 November 2011
0 comments
Tornado, Mozilla
BrowserID is a new single sign-on initiative lead by Mozilla that takes a very refreshing approach to single sign-on. It's basically like OpenID except better and similar to the OAuth solutions from Google, Twitter, Facebook, etc but without being tied to those closed third-parties.
At the moment, BrowserID is ready for production (I have it on Kwissle) but the getting started docs is still something that is under active development (I'm actually contributing to this).
Anyway, I thought I'd share how to integrate it with Tornado
First, you need to do the client-side of things. I use jQuery but that's not a requirement to be able to use BrowserID. Also, there are different "patterns" to do login. Either you have a header that either says "Sign in"/"Hi Your Username". Or you can have a dedicated page (e.g. mysite.com/login/). Let's, for simplicity sake, pretend we build a dedicated page to log in. First, add the necessary HTML:
<a href="#" id="browserid" title="Sign-in with BrowserID">
<img src="/images/sign_in_blue.png" alt="Sign in">
</a>
<script src="https://browserid.org/include.js" async></script>
Next you need the Javascript in place so that clicking on the link above will open the BrowserID pop-up:
function loggedIn(response) {
location.href = response.next_url;
/* alternatively you could do something like this instead:
$('#header .loggedin').show().text('Hi ' + response.first_name);
...or something like that */
}
function gotVerifiedEmail(assertion) {
// got an assertion, now send it up to the server for verification
if (assertion !== null) {
$.ajax({
type: 'POST',
url: '/auth/login/browserid/',
data: { assertion: assertion },
success: function(res, status, xhr) {
if (res === null) {}//loggedOut();
else loggedIn(res);
},
error: function(res, status, xhr) {
alert("login failure" + res);
}
});
}
else {
//loggedOut();
}
}
$(function() {
$('#browserid').click(function() {
navigator.id.getVerifiedEmail(gotVerifiedEmail);
return false;
});
});
Next up is the server-side part of BrowserID. Your job is to take the assertion that is given to you by the AJAX POST and trade that with https://browserid.org for an email address:
import urllib
import tornado.web
import tornado.escape
import tornado.httpclient
...
@route('/auth/login/browserid/') # ...or whatever you use
class BrowserIDAuthLoginHandler(tornado.web.RequestHandler):
def check_xsrf_cookie(self): # more about this later
pass
@tornado.web.asynchronous
def post(self):
assertion = self.get_argument('assertion')
http_client = tornado.httpclient.AsyncHTTPClient()
domain = self.request.host
url = 'https://browserid.org/verify'
data = {
'assertion': assertion,
'audience': domain,
}
response = http_client.fetch(
url,
method='POST',
body=urllib.urlencode(data),
callback=self.async_callback(self._on_response)
)
def _on_response(self, response):
struct = tornado.escape.json_decode(response.body)
if struct['status'] != 'okay':
raise tornado.web.HTTPError(400, "Failed assertion test")
email = struct['email']
self.set_secure_cookie('user', email,
expires_days=1)
self.set_header("Content-Type", "application/json; charset=UTF-8")
response = {'next_url': '/'}
self.write(tornado.escape.json_encode(response))
self.finish()
Now that should get you up and running. There's of couse a tonne of things that can be improved. Number one thing to improve is to use XSRF on the AJAX POST. The simplest way to do that would be to somehow dump the XSRF token generated into your page and include it in the AJAX POST. Perhaps something like this:
<script>
var _xsrf = '{{ xsrf_token }}';
...
function gotVerifiedEmail(assertion) {
// got an assertion, now send it up to the server for verification
if (assertion !== null) {
$.ajax({
type: 'POST',
url: '/auth/login/browserid/',
data: { assertion: assertion, _xsrf: _xsrf },
...
</script>
Another thing that could obviously do with a re-write is the way users are handled server-side. In the example above I just set the asserted user's email address in a secure cookie. More realistically, you'll have a database of users who you match by email address but instead store their database ID in a cookie or something like that.
What's so neat about solutions such as OpenID, BrowserID, etc. is that you can combine two things in one process: Sign-in and Registration. In your app, all you need to do is a simple if statement in the code like this:
user = self.db.User.find_by_email(email)
if not user:
user = self.db.User()
user.email = email
user.save()
self.set_secure_cookie('user', str(user.id))
Hopefully that'll encourage a couple of more Tornadonauts to give BrowserID a try.
Too Cool For Me?
25 September 2011
0 comments
Tornado
http://toocoolfor.me
Too Cool For Me? is a fun little side-project I've been working on. It's all about and only for Twitter. You login, then install a bookmarklet then when browsing twitter you can see who follows you and who is too cool for you.
For me it's a chance to try some new tech and at the same time scratch an itch I had. The results can be quite funny but also sad too when you realise that someone uncool isn't following you even though you follow him/her.
The code is open source and available on Github and at least it might help people see how to do a web app in Tornado using MongoDB and asynchronous requests to the Twitter API
Goodies from tornado-utils - part 3: send_mail
24 September 2011
0 comments
Tornado
https://github.com/peterbe/tornado-utils/tree/master/tornado_utils/send_mail
This is Part 3 in a series of blogs about various bits and pieces in the tornado-utils package. Part 1 is here and part 2 is here
send_mail
First of all, I should say: I didn't write much of this code. It's copied from Django and modified to work in any Tornado app. It hinges on the same idea as Django that you have to specify what backend you want to use. A backend is an importable string pointing to a class that has the send_messages method.
To begin, here's a sample use case inside a request handler:
class ContactUs(tornado.web.RequestHandler):
def post(self):
msg = self.get_argument('msg')
# NB: you might want to set this once and for all in something
# like self.application.settings['email_backend']
backend = 'tornado_utils.send_mail.backends.smtp.EmailBackend'
send_email(backend,
"New contact form entry",
msg + '\n\n--\nFrom our contact form\n',
'noreply@example.com',
['webmaster@example.com'],
)
self.write("Thanks!")
The problem is that SMTP is slow. Even though, in human terms, it's fast, it's still too slow for a non-blocking server that Tornado is. Taking 1-2 seconds to send a message over SMTP means it's blocking every other request to Tornado for 1-2 seconds. The solution is instead save the message on disk in pickled form and use a cron job to pick up the messages and send them by SMTP instead, outside the Tornado process. First do this re-write:
...
def post(self):
msg = self.get_argument('msg')
- backend = 'tornado_utils.send_mail.backends.smtp.EmailBackend'
+ backend = 'tornado_utils.send_mail.backends.pickle.EmailBackend'
...
Now, write a cron job script that looks something like this:
# send_pickled_messages.py
DRY_RUN = False
def main():
from tornado_utils.send_mail import config
filenames = glob(os.path.join(config.PICKLE_LOCATION, '*.pickle'))
filenames.sort()
if not filenames:
return
from tornado_utils.send_mail import backends
import cPickle
if DRY_RUN:
EmailBackend = backends.console.EmailBackend
else:
EmailBackend = backends.smtp.EmailBackend
max_count = 10
filenames = filenames[:max_count]
messages = [cPickle.load(open(x, 'rb')) for x in filenames]
backend = EmailBackend()
backend.send_messages(messages)
if not DRY_RUN:
for filename in filenames:
os.remove(filename)
That code just above is butchered from a more comprehensive script I have but you get the idea. Writing to a pickle file is so fast it's in the lower milliseconds region. However, it depends on disk IO so if you need more speed, write a simple backend that writes instead of saving pickles on disk, make it write to a fast in-memory database like Redis or Memcache.
The code isn't new and it's been battle tested but it's only really been battle tested in the way that my apps use it. So you might stumble across bugs if you use it in a way I haven't tested. However, the code is Open Source and happily available for you to help out and improve.
Goodies from tornado-utils - part 2: tornado_static
22 September 2011
0 comments
Tornado
https://github.com/peterbe/tornado-utils/blob/master/tornado_utils/tornado_static.py
This is Part 2 in a series of blogs about various bits and pieces in the tornado-utils package. Part 1 is here
tornado_static
This code takes care of two things: 1) optimizing your static resources and 2) bundling and serving them with unique names so you can cache aggressively.
The trick is to make your development environment such that there's no need to do anything when in "debug mode" but when in "production mode" it needs to be perfect. Which files (e.g. jquery.js or style.css) you use and bundle is up to you and it's something you control from the templates in your Tornado app. Not a config setting because, almost always, which resources (aka. assets) you need is known and relevant only to the templates where you're working.
Using UI modules in Tornado requires a bit of Tornado-fu but here's one example and here is another (untested) example:
# app.py
import tornado.web
from tornado_utils.tornado_static import (
StaticURL, Static, PlainStaticURL, PlainStatic)
class Application(tornado.web.Application):
def __init__(self):
ui_modules = {}
if options.debug:
ui_modules['Static'] = PlainStatic
ui_modules['StaticURL'] = PlainStaticURL
else:
ui_modules['Static'] = Static
ui_modules['StaticURL'] = StaticURL
app_settings = dict(
template_path="templates",
static_path="static",
ui_modules=ui_modules,
debug=options.debug,
UGLIFYJS_LOCATION='~/bin/uglifyjs',
CLOSURE_LOCATION="static/compiler.jar",
YUI_LOCATION="static/yuicompressor-2.4.2.jar",
cdn_prefix="cdn.mycloud.com",
)
handlers = [
(r"/", HomeHandler),
(r"/entry/([0-9]+)", EntryHandler),
]
super(Application, self).__init__(handlers, **app_settings)
def main(): # pragma: no cover
tornado.options.parse_command_line()
http_server = tornado.httpserver.HTTPServer(Application())
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == "__main__":
main()
Note! If you're looking to optimize your static resources in a Tornado app you probably already have a "framework" for setting up UI modules into your app. The above code is just to wet your appetite and to show how easy it is to set up. The real magic starts to happen in the template code. Here's a sample implementation:
<!doctype html>
<html>
<head>
<title>{% block title %}{{ page_title }}{% end %}</title>
<meta charset="utf-8">
{% module Static("css/ext/jquery.gritter.css", "css/style.css") %}
</head>
<body>
<header>...</header>
{% block content %}
{% end %}
{% module Static("js/ext/head.load.min.js") %}
<script>
var POST_LOAD = '{% module StaticURL("js/dojo.js", "js/dojo.async.js") %}';
</script>
</body>
</html>
What you get when run is a template that looks like this:
<!doctype html>
<html>
<head>
<title>My title</title>
<meta charset="utf-8">
<link rel="stylesheet" type="text/css" href="//cdn.mycloud.com/combined/jquery.gritter.css.style.1313206609.css">
</head>
...
(Note that this will create a whitespace optimized filed called "jquery.gritter.css.style.1313206609.css" in "/tmp/combined")
Have a play and see if it makes sense in your app. I do believe this can do with some Open Source love but so far it works great for me on Kwissle, DoneCal and TooCoolFor.Me
Goodies from tornado-utils - part 1: TestClient
20 September 2011
0 comments
Tornado
https://github.com/peterbe/tornado-utils/blob/master/tornado_utils/http_test_client.py
This is Part 1 in a series of blogs about various bits and pieces in the tornado-utils package.
tornado-utils is the result of me working on numerous web apps in Tornado that after too much copy-and-paste from project to project eventually became a repo standing on its own two legs.
TestClient
This makes it possible to write integration tests that executes GET and POST requests on your app similarly to how Django does it. Example implementation:
# tests/base.py
from tornado_utils.http_test_client import TestClient, HTTPClientMixin
from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase
class BaseAsyncTestCase(AsyncHTTPTestCase, LogTrapTestCase):
pass
class BaseHTTPTestCase(BaseAsyncTestCase, HTTPClientMixin):
def setUp(self):
super(BaseHTTPTestCase, self).setUp()
self.client = TestClient(self)
# tests/test_handlers.py
from .base import BaseHTTPTestCase
class HandlersTestCase(BaseHTTPTestCase):
def setUp(self):
super(HandlersTestCase, self).setUp()
self._create_user('peterbe', 'secret') # use your imagination
def test_homepage(self):
response = self.client.get('/')
self.assertEqual(response.code, 200)
self.assertTrue('stranger' in response.body)
data = {'username': 'peterbe', 'password': 'secret'}
response = self.client.post('/login/', data)
self.assertEqual(response.code, 302)
response = self.client.get('/')
self.assertEqual(response.code, 200)
self.assertTrue('stranger' not in response.body)
self.assertTrue('hi peterbe' in response.body)
You can see a sample implementation of this here
Note that this was one of the first pieces of test code I wrote in my very first Tornado app and it's not unlikely that some assumptions and approaches are naive or even wrong but what we have here works and it makes the test code very easy to read. All it basically does is wraps the http_client.fetch(...) call and also maintains a bucket of cookies
I hope it can be useful to someone new to writing tests in Tornado.
Launching Kwissle.com
04 June 2011
4 comments
Tornado
http://kwissle.com
For the past couple of months I've been working on a little fun side-project that I've decided to call: Kwissle
It's an online game where you're paired up randomly with someone else who's online on the site at the same time and you enter a battle of general knowledge questions.
The rules are simple:
- each battle has 10 questions
- you get 15 seconds per question
- if you type in the right answer (spelling mistakes are often allowed) you get 3 points
- if you load the alternatives and get it right you get 1 point
- you have to be fast or else your opponent might steal the question
- whoever has the most points win!
The app has been quite a technical challenge because it's real-time. Like a chat. To accomplish this, I've decided to use the socket.io library with Tornado which means that if your browser supports it, it's using WebSockets or else if falls back on Flash sockets.
Because there is no easy way to source questions, I've instead opted to do it by crowd sourcing. What that simply means is that YOU help out adding the questions. You can also help out by reviewing other peoples questions before I eventually publish them.
Bear with me though; this is an early alpha release and it just about works. There are a tonne of features and improvements I want to add to make it more fun and solid but, like I said, this is a side-project so the only time I get to work on it is late evenings and weekends. The source code is not open but I'm going to try to be entirely open with what's going on on a tumblr blog
So, without further ado; head over and start playing
TornadoGists.org - launched and ready!
06 April 2011
1 comment
Python, Tornado
http://tornadogists.org/
Today Felinx Lee and I launched TornadoGists.org which is a site for discussing gists related to Tornado (python web framework open sourced by Facebook).
Everyone in the Tornado community seems to solve similar problems in different ways. Oftentimes, these solutions are just a couple of lines or so and not something you can really turn into a full package with setup.py and everything.
Sharing a snippet of code is a great way to a) help other people and b) to get feedback on your solutions.
The goal is to make it a very open and active project with lots of contributors. I'll be accepting and reviewing all forks but hopefully control will be opened up to all Tornado developers. Also, since the code is quite generic to any open source project Felinx and I might one day port this to rubygists.org or lispgists.org or something like that. After all, Github does all the heavy lifting and we just wrap it up nicely.
Welcome to the world: DoneCal.com
22 November 2010
0 comments
Python, Tornado
http://donecal.com/
After about two months of evening hacking I'm finally ready to release my latest project: DoneCal.com
It's a simple calendar that doesn't get in your way. You just click on a day and type what you did that day. DoneCal can be an ideal replacement to boring spreadsheet-like timesheets. And unlike regular timesheets/timetrackers with tags you immediately get statistics about how you've spent your time.
I'm personally excited about the Bookmarklet because I practically live in my webbrowser and now I can quickly type what I've just done (could be a piece of support work for a client) with one single click.
If you're a project manager trying to track what your developers are working on, ask them to start tracking time on DoneCal and then ask them to share their calendar with you. They can set up their share so that it only shares on relevant tags.
I'm going to improving it more and more as feedback comes in. Hopefully later this week I'm going to be writing about the technical side of this since this is my first web app built with the uber-fast Tornado framework
My tricks for using AsyncHTTPClient in Tornado
13 October 2010
0 comments
Python, Tornado
I've been doing more and more web development with Tornado recently. It's got an awesome class for running client HTTP calls in your integration tests. To run a normal GET it looks something like this:
from tornado.testing import AsyncHTTPTestCase
class ApplicationTestCase(AsyncHTTPTestCase):
def get_app(self):
return app.Application(database_name='test', xsrf_cookies=False)
def test_homepage(self):
url = '/'
self.http_client.fetch(self.get_url(url), self.stop)
response = self.wait()
self.assertTrue('Click here to login' in response.body)
Now, to run a POST request you can use the same client. It looks something like this:
def test_post_entry(self):
url = '/entries'
data = dict(comment='Test comment')
from urllib import urlencode
self.http_client.fetch(self.get_url(url), self.stop,
method="POST",
data=urlencode(data))
response = self.wait()
self.assertEqual(response.code, 302)
That's fine but it gets a bit verbose after a while. So instead I've added this little cute mixin class:
from urllib import urlencode
class HTTPClientMixin(object):
def get(self, url, data=None, headers=None):
if data is not None:
if isinstance(data, dict):
data = urlencode(data)
if '?' in url:
url += '&%s' % data
else:
url += '?%s' % data
return self._fetch(url, 'GET', headers=headers)
def post(self, url, data, headers=None):
if data is not None:
if isinstance(data, dict):
data = urlencode(data)
return self._fetch(url, 'POST', data, headers)
def _fetch(self, url, method, data=None, headers=None):
self.http_client.fetch(self.get_url(url), self.stop, method=method,
body=data, headers=headers)
return self.wait()
Now you can easily write some brief and neat tests:
class ApplicationTestCase(AsyncHTTPTestCase, HTTPClientMixin):
def get_app(self):
return app.Application(database_name='test', xsrf_cookies=False)
def test_homepage(self):
response = self.get('/')
self.assertTrue('Click here to login' in response.body)
def test_post_entry(self):
# rendering the homepage creates a user and sets a cookie
response = self.get('/')
user_id_cookie = re.findall('user_id=([\w\|]+);',
response.headers['Set-Cookie'])[0]
cookie = 'user_id=%s;' % user_id_cookie
import base64
guid = base64.b64decode(user_id_cookie.split('|')[0])
self.assertEqual(db.users.User.find(
{'_id':ObjectId(user_id_cookie)}).count(), 1)
data = dict(comment='Test comment')
response = self.post('/entries', data, headers={'Cookie': cookie})
self.assertEqual(response.code, 302)
self.assertTrue('/thanks' in response.headers['Location'])
So far it's just a neat wrapper to save me some typing and it makes the actual tests look a lot neater. I haven't tested this in anger yet and there might be several interesting corner cases surrounding headers and POST data and what not. Hopefully people can chip in and share ideas on this snippet and perhaps I can fork this into Tornado's core