Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
DB-backed Cached Session in Python
from django.conf import settings
from django.contrib.sessions.backends.base import SessionBase
from django.core.cache import cache
from PHPSerialize import PHPSerialize
from PHPUnserialize import PHPUnserialize
from session_models import SessionEntry
from datetime import timedelta
from sqlalchemy import func, text
from sharding import Session
class SessionStore(SessionBase):
"""Custom database-backed sessions with a memcached layer.
Also reads & writes the session information in a PHP-compatible format.
"""
MAX_CACHE_AGE = timedelta(seconds=settings.SESSION_MAX_AGE)
MIN_CACHE_AGE = timedelta(seconds=settings.SESSION_MIN_AGE)
CACHE_SLEEP = MAX_CACHE_AGE - MIN_CACHE_AGE
def __init__(self, session_key=None):
super(SessionStore, self).__init__(session_key)
def load(self):
sess = None
content = cache.get(self.session_key)
expired = (cache.get(self.expires_key) == None)
if not content:
sess = self._get_db_session(self.session_key, exclude_expired=True)
if sess:
content = sess.content
expired = sess.expires_in < SessionStore.MIN_CACHE_AGE
if not content:
return {}
cache.set(self.session_key, content, SessionStore.MAX_CACHE_AGE.seconds)
session_dictionary = PHPUnserialize().session_decode(content.encode('utf-8'))
if expired:
if self._should_persist(session_dictionary):
sess = sess or self._get_db_session(self.session_key, create_if_missing=True)
sess.expires = text("current_timestamp + interval '%d seconds'" % SessionStore.MAX_CACHE_AGE.seconds)
sess.content = content
sess.flush()
cache.set(self.expires_key, "1", SessionStore.CACHE_SLEEP.seconds)
else:
cache.delete(self.expires_key)
return session_dictionary
def save(self):
session_dictionary = self._get_session()
content = PHPSerialize().session_encode(session_dictionary).decode('utf-8')
cache.set(self.session_key, content, SessionStore.MAX_CACHE_AGE.seconds)
if self._should_persist(session_dictionary):
sess = self._get_db_session(self.session_key, create_if_missing=True)
sess.expires = text("current_timestamp + interval '%d seconds'" % SessionStore.MAX_CACHE_AGE.seconds)
sess.content = content
sess.flush()
cache.set(self.expires_key, "1", SessionStore.CACHE_SLEEP.seconds)
else:
cache.delete(self.expires_key)
def exists(self, session_key):
return cache.get(session_key) is not None or \
self._get_db_session(session_key) is not None
def delete(self, session_key):
cache.delete(session_key)
cache.delete(session_key + '_expires')
sess = self._get_db_session(session_key)
if sess:
sess.delete()
def _get_db_session(self, session_key, create_if_missing=False, exclude_expired=False):
if not self._database_sessions_available():
return None
query = Session.query(SessionEntry).filter(SessionEntry.session_id == session_key)
if exclude_expired:
query = query.filter(SessionEntry.expires >= func.current_timestamp())
sess = query.first()
if not sess and create_if_missing:
sess = SessionEntry(session_id = session_key)
return sess
def _should_persist(self, session_dictionary):
return self._database_sessions_available() and \
session_dictionary.get('user_id', 'anon') != 'anon'
def _database_sessions_available(self):
# Because sharding allows us to move between multiple databases, we
# need to know if we've been able to set up a connection to our "home"
# shard. If we haven't, we can't try to use any database sessions
# because we run the risk of carrying our sessions between shards that
# way, and potentially accessing old information.
#
# When we configure the session, we also set has_home on the
# SessionEntry class to specify whether we explicitly bound it to
# something.
return SessionEntry.has_home
@property
def expires_key(self):
return self.session_key + '_expires'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment