public

Tornado Sessions with Redis!

So, first let me say... I love Tornado [http://www.tornadoweb.org/en/stable/].  Love it.  It's simple, it's easy, it's asynchronous and non-blocking! What's not to love? For me,

10 years ago

Latest Post Maximizing DevOps Efficiency: Best Practices, KPIs, and Realtime Feedback by Mike Moore public

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!

Mike Moore

Published 10 years ago