I played around with the code a bit. Nothing finished but it gave me more of an idea of what is feasable. mso@oz.net wrote: >- SQLSessionStore also assumes the connection is transaction safe. DB-API >says you cannot call conn.rollback() on an unsafe connection, yet the > >- SQLSessionStore also assumes the .execute() substutions use "%(var)s" >syntax. That would fail on some database systems. But since it affects > > I'd leave the implementation as is and put a docstring saying tested with psycopg and MySQLdb, should work with all DB-API modules that support connection.rollback() and "%(var)s" substitution syntax, you can subclass it for other databases. It would be nice to also test it against sqlite at least. >- You can use SQL REPLACE to avoid the UPDATE/INSERT 'if'. > > If PostgreSQL and sqlite have this.... > - Locking is a general problem; perhaps hooks belongs in the I'm not sure the .lock() and .unlock() methods will gain anything. There's two kinds of locking: thread locking and file locking. fcntl file locking requires knowledge of the file descriptor (file.fileno() on an open file). Who knows what Windows and Mac file locking might require. Thread locking requires a pre- existing mutex. You can't allocate it in .lock() except in the cheesy "if not hasattr(self, 'mutex')" way of initializing. So already we see cases where: - .lock() is called too early -- before the file is open. - .lock()/.unlock() require arguments specific to the implementation. - You may need to set a mutex attribute in .__init__(). - The locking/unlocking code is often one-liners; it would look better inlined in such short methods. - The try/finally blocks are a drag for implementations that don't use locking, they sometimes take more lines of code than the rest of the method! I'm tempted to say get rid of .lock/.unlock and instead put a warning in the docstring if the class is not thread/multiprocess safe. We can also offer subclasses with locking (ThreadedDurusSessionStore in the example). Attached are some ideas. The module is unfinished so don't try to run it as-is.
""" *** UNFINISHED EXAMPLE, NOT READY FOR USE *** A session manager & storage API for making sessions persistent in Quixote 2.0. You need only subclass SessionStore and pass it into PersistentSessionManager as the first argument. """ try: from thread import allocate_lock except ImportError: from dummy_thread import allocate_lock from quixote import get_request, get_publisher, get_cookie, get_response from quixote.session import Session from quixote.util import randbytes class SessionStore: """ Persistent Session storage API for PersistentSessionManager. Subclass this class & provide implementations of load_session, save_session, and delete_session, and voila, persistent sessions! """ def load_session(self, id, default=None): raise NotImplementedError() def save_session(self, session): raise NotImplementedError() def delete_session(self, session): raise NotImplementedError() def has_session(self, id): return self.load_session(id, None) # --- class PersistentSessionManager deleted for brevity; insert it here --- import os from cPickle import dump, load class DirectorySessionStore(SessionStore): """ Store sessions in individual files within a directory. The filename is the session cookie. """ def __init__(self, directory, create=False): """ __init__ takes a directory name, with an option to create it if it's not already there. """ directory = os.path.abspath(directory) # make sure the directory exists: if not os.path.exists(directory): if create: os.mkdir(directory) else: raise Exception("error, '%s' does not exist." % (directory,)) # is it actually a directory? if not os.path.isdir(directory): raise Exception("error, '%s' is not a directory." % (directory,)) self.directory = directory def _make_filename(self, id): """ Build the filename from the session ID. """ return os.path.join(self.directory, id) def load_session(self, id, default=None): """ Load the pickled session from a file. """ filename = self._make_filename(id) try: f = open(filename) obj = load(f) f.close() except IOError: obj = default return obj def save_session(self, session): """ Pickle the session and save it into a file. """ filename = self._make_filename(session.id) f = open(filename, 'w') dump(session, f,) f.close() def delete_session(self, session): """ Delete the session file. """ filename = self._make_filename(session.id) os.unlink(filename) class SQLSessionStore(SessionStore): """ Store pickled sessions in an SQL database. The database must have the following table: CREATE TABLE sessions ( id TEXT NOT NULL, pickle TEXT NOT NULL ); # @@MO: id should be primary key. # @@MO: pickle being a text field limits us to pickle protocol 1. This implementation has been tested with psycopg and MySQLdb. It should work with any DB-API module that supports connection.rollback() and "%(var)s" substitution style. You can subclass it for databases not meeting this requirement. """ def __init__(self, conn): self.conn = conn def load_session(self, id, default=None): import cPickle as pickle self.conn.rollback() c = self.conn.cursor() c.execute('SELECT pickle FROM sessions WHERE id=%(id)s', dict(id=id)) if c.rowcount == 0: if len(rows) == 0: return default pck = c.fetchone()[0] obj = pickle.loads(pck) return obj def save_session(self, session): from cPickle import pickle pck = pickle.dumps(session) self.conn.rollback() c = self.conn.cursor() # decide whether to INSERT or UPDATE: # @@MO: Could use SQL REPLACE instead. if self.has_session(session.id): sql = 'UPDATE sessions SET pickle=%(p)s WHERE id=%(id)s' else: sql = 'INSERT INTO sessions (id, pickle) VALUES (%(id)s, %(p)s)' c.execute(sql, dict(id=session.id, p=pck)) self.conn.commit() def delete_session(self, session): self.conn.rollback() c = self.conn.cursor() c.execute('DELETE FROM sessions WHERE id=%(id)s', dict(id=session.id)) self.conn.commit() class ShelveSessionStore(SessionStore): """ Open a 'shelve' dictionary with the given filename, and store sessions in it. Shelve is not thread safe or multiprocess safe. See the "Restrictions" section for the shelve module in the Python Library Reference for information about file locking. """ def __init__(self, filename): self.filename = filename def open(self): import shelve return shelve.open(self.filename, 'c') def load_session(self, id, default=None): db = self.open() return db.get(id, default) def delete_session(self, session): db = self.open() del db[session.id] def save_session(self, session): db = self.open() db[session.id] = session class DurusSessionStore(SessionStore): """ A session store for Durus, a simple object database. Unlike the dulcinea Durus session store, session objects themselves are *not* subclasses of Persistent; here they are managed by DurusSessionStore directly. Do not use this class in multithreaded applications; use ThreadedDurusSessionStore instead. """ def __init__(self, connection): from durus.persistent_dict import PersistentDict self.connection = connection root = connection.get_root() sessions_dict = root.get('sessions') if sessions_dict is None: sessions_dict = PersistentDict() root['sessions'] = sessions_dict connection.commit() self.sessions_dict = sessions_dict def load_session(self, id, default=None): self.connection.abort() return self.sessions_dict.get(id, default) def delete_session(self, session): del self.sessions_dict[session.id] self.connection.commit() def save_session(self, session): self.sessions_dict[session.id] = session self.connection.commit() class ThreadedMixIn: def init_threading(self): self.lock = allocate_lock() def synchronize(self, func, *args, **kw): self.lock.acquire() try: ret = func(*args, **kw) finally: self.lock.release() return ret class ThreadedDurusSessionStore(ThreadedMixIn, DurusSessionStore): """A thread safe version of DurusSessionStore.""" def __init__(self, connection): self.init_threading() self.synchronize(DurusSessionStore.__init__, self, connection) def load_session(self, id, default=None): self.synchronize(DurusSessionStore.load_session, self, id, default) def delete_session(self, session): self.synchronize(DurusSessionStore.delete_session, self, session) def save_session(self, session): self.synchronize(DurusSessionStore.save_session, self, session)