"""Session store using the Memcached memory object caching system."""
from warnings import warn
try:
from cPickle import HIGHEST_PROTOCOL as maxPickleProtocol
except ImportError:
from pickle import HIGHEST_PROTOCOL as maxPickleProtocol
try:
import memcache
except Exception:
raise ImportError("For using Memcached sessions,"
" python-memcached must be installed.")
from MiscUtils import NoDefault
from SessionStore import SessionStore
debug = False
class SessionMemcachedStore(SessionStore):
"""A session store using Memached.
Stores the sessions in a single Memcached store using 'last write wins'
semantics. This increases fault tolerance and allows server clustering.
In clustering configurations with concurrent writes for the same
session(s) the last writer will always overwrite the session.
The keys are prefixed with a configurable namespace, allowing you to
store other data in the same Memcached system.
Cleaning/timing out of sessions is performed by Memcached itself
since no one app server can know about the existence of all sessions or
the last access for a given session. Besides it is built in Memcached
functionality. Consequently, correct sizing of Memcached is necessary
to hold all user's session data.
Due to the way Memcached works, methods requiring access to the keys
or for clearing the store do not work. You can configure whether you
want to ignore such calls or raise an error in this case. By default,
you will get a warning. It would be possible to emulate these functions
by storing additional data in the memcache, such as a namespace counter
or the number or even the full list of keys. However, if you are using
more than one app server instance, this would require fetching that data
every time, since we cannot know whether another instance changed it.
So we refrained from doing such sophisticated trickery and instead kept
the implementation intentionally very simple and fast.
You need to install python-memcached to be able to use this module:
http://www.tummy.com/Community/software/python-memcached/
You also need a Memcached server: http://memcached.org
Contributed by Steve Schwarz, March 2010.
Small improvements by Christoph Zwerschke, April 2010.
"""
def __init__(self, app):
SessionStore.__init__(self, app)
self._servers = app.setting('MemcachedServers', ['localhost:11211'])
self._sessionTimeout = app.setting(
'SessionTimeout', 180) * 60
self._namespace = app.setting(
'MemcachedNamespace', 'WebwareSession') or ''
self._onIteration = app.setting('MemcachedOnIteration', 'Warning')
self._client = memcache.Client(self._servers,
debug=debug, pickleProtocol=maxPickleProtocol)
def __len__(self):
"""Return the number of sessions in the store.
Not supported by Memcached (see FAQ for explanation).
"""
if debug:
print ">> len()"
return len(self.keys())
def __getitem__(self, key):
"""Get a session item, reading it from the store."""
if debug:
print ">> getitem(%s)" % key
try:
value = self._client.get(self.mcKey(key))
except Exception:
value = None
if value is None:
raise KeyError(key)
return value
def __setitem__(self, key, value):
"""Set a session item, writing it to the store."""
if debug:
print ">> setitem(%s, %s)" % (key, value)
dirty = value.isDirty()
if self._alwaysSave or dirty:
if dirty:
value.setDirty(False)
try:
if not self._client.set(self.mcKey(key), value,
time=self._sessionTimeout):
raise ValueError("Setting value in the memcache failed.")
except Exception, exc:
if dirty:
value.setDirty()
print "Error saving session '%s' to memcache: %s" % (key, exc)
self.application().handleException()
def __delitem__(self, key):
"""Delete a session item from the store.
Note that in contracts with SessionFileStore,
not finding a key to delete isn't a KeyError.
"""
if debug:
print ">> delitem(%s)" % key
session = self[key]
if not session.isExpired():
session.expiring()
try:
if not self._client.delete(self.mcKey(key)):
raise ValueError("Deleting value from the memcache failed.")
except Exception, exc:
print "Error deleting session '%s' from memcache: %s" % (key, exc)
self.application().handleException()
def __contains__(self, key):
"""Check whether the session store has a given key."""
if debug:
print ">> contains(%s)" % key
try:
return self._client.get(self.mcKey(key)) is not None
except Exception:
return False
def __iter__(self):
"""Return an iterator over the stored session keys.
Not supported by Memcached (see FAQ for explanation).
"""
if debug:
print ">> iter()"
onIteration = self._onIteration
if onIteration:
err = 'Memcached does not support iteration.'
if onIteration == 'Error':
raise NotImplementedError(err)
else:
warn(err)
return iter([])
def keys(self):
"""Return a list with the keys of all the stored sessions.
Not supported by Memcached (see FAQ for explanation).
"""
if debug:
print ">> keys()"
return [key for key in self]
def clear(self):
"""Clear the session store, removing all of its items.
Not supported by Memcached. We could emulate this by incrementing
an additional namespace counter, but then we would need to fetch
the current counter from the memcache before every access in order
to keep different app server instances in sync.
"""
if debug:
print ">> clear()"
if self._onIteration:
err = 'Memcached does not support clearing the store.'
if self._onIteration == 'Error':
raise NotImplementedError(err)
else:
warn(err)
def setdefault(self, key, default=None):
"""Return value if key available, else default (also setting it)."""
if debug:
print ">> setdefault(%s, %s)" % (key, default)
try:
return self[key]
except KeyError:
self[key] = default
return default
def pop(self, key, default=NoDefault):
"""Return value if key available, else default (also remove key)."""
if debug:
print ">> pop(%s, %s)" % (key, default)
if default is NoDefault:
value = self[key]
del self[key]
return value
else:
try:
value = self[key]
except KeyError:
return default
else:
del self[key]
return value
def storeSession(self, session):
"""Save potentially changed session in the store."""
if debug:
print ">> storeSession(%s)" % session
self[session.identifier()] = session
def storeAllSessions(self):
"""Permanently save all sessions in the store.
Should be used (only) when the application server is shut down.
This closes the connection to the Memcached servers.
"""
if debug:
print ">> storeAllSessions()"
self._client.disconnect_all()
def cleanStaleSessions(self, task=None):
"""Clean stale sessions.
Memcached does this on its own, so we do nothing here.
"""
if debug:
print ">> cleanStaleSessions()"
def mcKey(self, key):
"""Create the real key with namespace to be used with Memcached."""
return self._namespace + key