Tornado Sessions with Redis!

So, first let me say... I love Tornado.  Love it.  It's simple, it's easy, it's asynchronous and non-blocking! What's not to love?

For me, I particularly have a fondness for Redis and using it for reliable web session storage and access.  Whether you love it or hate it, I could care less, but I personally like it and it suits my needs quite well for most projects.

So taking these two technologies and using the awesome power of Tornado, and the great "No-SQL" (p.s. "No-SQL" is a stupid term) Redis and merging the two sounded like a pretty great idea.  There remains only one problem.... Tornado really doesn't have a great native session handler!  So I said to myself... Go forth and build Mike! And that leads us here to our session.py file:

try:
    import cPickle as pickle
except:
    import pickle
from uuid import uuid4
import time

class RedisSessionStore:
    def __init__(self, redis_connection, **options):
        self.options = {
            'key_prefix': 'session',
            'expire': 5184000,
        }
        self.options.update(options)
        self.redis = redis_connection

    def prefixed(self, sid):
        return '%s:%s' % (self.options['key_prefix'], sid)

    def generate_sid(self, ):
        return uuid4().get_hex()

    def get_session(self, sid, name):
        data = self.redis.hget(self.prefixed(sid), name)
        session = pickle.loads(data) if data else dict()
        return session

    def set_session(self, sid, session_data, name):
        expiry = self.options['expire']
        self.redis.hset(self.prefixed(sid), name, pickle.dumps(session_data))
        if expiry:
            self.redis.expire(self.prefixed(sid), expiry)

    def delete_session(self, sid):
        self.redis.delete(self.prefixed(sid))

class Session:
    def __init__(self, session_store, sessionid=None):
        self._store = session_store
        self._sessionid = sessionid if sessionid else self._store.generate_sid()
        self._sessiondata = self._store.get_session(self._sessionid, 'data')
        self.dirty = False

    def clear(self):
        self._store.delete_session(self._sessionid)

    def access(self, remote_ip):
        access_info = {'remote_ip':remote_ip, 'time':'%.6f' % time.time()}
        self._store.set_session(
                self._sessionid,
                'last_access',
                pickle.dumps(access_info)
                )

    def last_access(self):
        access_info = self._store.get_session(self._sessionid, 'last_access')
        return pickle.loads(access_info)

    @property
    def sessionid(self):
        return self._sessionid

    def __getitem__(self, key):
        return self._sessiondata[key]

    def __setitem__(self, key, value):
        self._sessiondata[key] = value
        self._dirty()

    def __delitem__(self, key):
        del self._sessiondata[key]
        self._dirty()

    def __len__(self):
        return len(self._sessiondata)

    def __contains__(self, key):
        return key in self._sessiondata

    def __iter__(self):
        for key in self._sessiondata:
            yield key

    def __repr__(self):
        return self._sessiondata.__repr__()

    def __del__(self):
        if self.dirty:
            self._save()

    def _dirty(self):
        self.dirty = True

    def _save(self):
        self._store.set_session(self._sessionid, self._sessiondata, 'data')
        self.dirty = False

This simple session.py file is handy dandy for inclusion into your tornado project!  Let's take a look at what may be in your main app .py file:

from session import Session

# Our base handler
class BaseHandler(tornado.web.RequestHandler):
    def __init__(self,application, request,**kwargs):
        super(BaseHandler,self).__init__(application,request)

    def get_current_user(self):
        return self.session['user'] if self.session and 'user' in self.session else None

    @property
    def session(self):
        sessionid = self.get_secure_cookie(<< YOUR AUTH COOKIE NAME >>,None)
        if sessionid:
            return Session(self.application.session_store, sessionid)
        else:
            sess = Session(self.application.session_store, None)
            self.set_secure_cookie(<< YOUR AUTH COOKIE NAME >>,sess.sessionid)
            return sess

Now, this particular example only works with a local redis server... However, you can always pass new connection parameters in to connect to a remote store or cluster.

This is a quick draft with some future editing in mind, but should you find any errors, let me know and I'll make sure to update appropriately! Enjoy!