durusmail: quixote-users: Session persistence for Quixote 2.0
Session persistence for Quixote 2.0
2005-05-26
2005-05-27
2005-05-27
2005-05-27
2005-05-27
2005-05-27
2005-05-27
2005-05-27
2005-05-28
2005-05-29
2005-05-29
2005-05-29
2005-05-28
2005-05-28
2005-05-29
2005-05-29
Sessions in mod_python (was [Quixote-users] Session persistence...
2005-05-29
Session persistence for Quixote 2.0
Mike Orr
2005-05-28
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)
reply