Commits

Alain Poirier committed 57ecad5

Sessions refactoring

Comments (0)

Files changed (6)

nagare/sessions/__init__.py

 
 
 class ExpirationError(LookupError):
-    """Raised when a session or a state is no longer valid
+    """Raised when a session or a state id is no longer valid
     """
     pass
+
+
+class SessionSecurityError(LookupError):
+    """Raised when the secure id of a session is not valid
+    """
+    pass

nagare/sessions/common.py

 """Base classes for the sessions management"""
 
 import random
-import cStringIO
-import cPickle
 
 import configobj
 
 from nagare import config
 from nagare.admin import reference
-from nagare.component import Component
-from nagare.sessions import ExpirationError
-from nagare.continuation import Tasklet
+from nagare.sessions import SessionSecurityError, serializer
 
 
 class State(object):
     """A state (objects graph serialized / de-serialized by a sessions manager)
     """
-    def __init__(self, sessions_manager, session_id, state_id, use_same_state):
+    def __init__(self, sessions_manager, session_id, state_id, secure_id, use_same_state):
         """Initialization
 
         In:
-          - ``sessions_manager`` -- the session manager of this state
+          - ``sessions_manager`` -- the manager of this state
           - ``session_id`` -- session id of this state
-          - ``state_id`` -- id of this state
-          - ``use_same_state`` -- is a copy of this state to create ?
+          - ``state_id`` -- id of this state (``None`` to create a new state)
+          - ``secure_id`` -- the secure number associated to the session
+          - ``use_same_state`` -- is a copy of this state to create?
         """
         self.sessions_manager = sessions_manager
         self.session_id = session_id
         self.state_id = state_id
+        self.secure_id = secure_id
         self.use_same_state = use_same_state
 
-        self.is_new = True         # Is the objects graph initialized ?
-        self.secure_id = None      # the secure number associated to the session
-        self.lock = None           # Session lock
-        self.data = None           # Objects graph
-        self.back_used = False     # Is this state a snapshot of a previous objects graph ?
+        self.back_used = False  # Is this state a snapshot of a previous objects graph?
+        self.lock = (sessions_manager.create_lock if self.is_new else sessions_manager.get_lock)(self.session_id)
 
-    def release(self):
-        """Release the session lock
-        """
-        self.lock.release()
+    @property
+    def is_new(self):
+        return self.state_id is None
 
     def sessionid_in_url(self, request, response):
         """Return the session and states ids to put into an URL
 
         In:
-          - ``request`` -- the web request object
-          - ``response`` -- the web response object
+          - ``request`` -- the web request
+          - ``response`` -- the web response
 
         Return:
-          - tuple (session id parameter, state id parameter)
+          - session id parameter
+          - state id parameter
         """
         return self.sessions_manager.sessionid_in_url(self.session_id, self.state_id, request, response)
 
 
         In:
           - ``h`` -- the current renderer
-          - ``request`` -- the web request object
-          - ``response`` -- the web response object
+          - ``request`` -- the web request
+          - ``response`` -- the web response
 
         Return:
           - the DOM tree
         """
         return self.sessions_manager.sessionid_in_form(self.session_id, self.state_id, h, request, response)
 
-    def create(self, secure_id):
-        """Initialized a new state, with an empty objects graph
+    def acquire(self):
+        """Lock the state
+        """
+        self.lock.acquire()  # Lock the session
+
+    def release(self):
+        """Release the state
+        """
+        self.lock.release()  # Release the session
+
+    def get_root(self):
+        """Retrieve the objects graph of this state
+
+        Return:
+         - the objects graph (``None`` if this state is new)
+        """
+        if self.state_id is None:
+            # New state
+            self.sessions_manager.create(self.session_id, self.secure_id, self.lock)
+            self.state_id = 0
+            data = None
+        else:
+            # Existing state
+            new_state_id, secure_id, data = self.sessions_manager.get_root(self.session_id, self.state_id)
+            if secure_id != self.secure_id:
+                raise SessionSecurityError
+
+            if not self.use_same_state:
+                self.back_used = (self.state_id != new_state_id - 1)
+                self.state_id = new_state_id
+
+        return data
+
+    def set_root(self, use_same_state, data):
+        """Store the objects graph of this state
 
         In:
-         - ``secure_id`` -- the secure number associated to the session
-        """
-        self.secure_id = secure_id
-        (self.state_id, self.lock) = self.sessions_manager.create_state(self.session_id, secure_id)
-
-    def get(self):
-        """Retrieve the state
-
-        Return:
-         - ``secure_id`` -- the secure number associated to the session
-        """
-        (new_state_id, self.lock, self.secure_id, self.data) = self.sessions_manager.get_state(self.session_id, self.state_id, self.use_same_state)
-
-        self.back_used = not self.use_same_state and (int(self.state_id) != (new_state_id - 1))
-
-        self.is_new = False
-        self.state_id = new_state_id
-
-        return self.secure_id
-
-    def set(self, use_same_state, data):
-        """Store the state
-
-        In:
-          - ``use_same_state`` -- is this state to be stored in the previous snapshot ?
+          - ``use_same_state`` -- is the objects graph to be stored in this state or in a new one?
           - ``data`` -- the objects graph
         """
-        self.sessions_manager.set_state(self.session_id, self.state_id, self.secure_id, self.use_same_state or use_same_state, data)
+        self.sessions_manager.set_root(self.session_id, self.state_id, self.secure_id, self.use_same_state or use_same_state, data)
 
     def delete(self):
-        """Delete this state
+        """Delete the session of this state
         """
         self.sessions_manager.delete(self.session_id)
 
 
-def persistent_id(o, clean_callbacks, callbacks, session_data, tasklets):
-    """The object with a ``_persistent_id`` attribute are stored into the session
-    not into the state snapshot
-
-    In:
-      - ``o`` -- object to check
-      - ``clean_callbacks`` -- do we have to forget the old callbacks?
-
-    Out:
-      - ``callbacks`` -- merge of the callbacks from all the components
-      - ``session_data`` -- dict of the objects to store into the session
-      - ``tasklets`` -- set of the serialized tasklets
-
-    Return:
-      - the persistent id or ``None``
-    """
-    r = None
-
-    id_ = getattr(o, '_persistent_id', None)
-    if id_ is not None:
-        session_data[id_] = o
-        r = str(id_)
-
-    elif type(o) is Tasklet:
-        tasklets.add(o)
-
-    elif isinstance(o, Component):
-        callbacks.update(o.serialize_callbacks(clean_callbacks))
-
-    return r
-
-
 class Sessions(object):
     """The sessions managers
     """
             'states_history': 'boolean(default=True)',
             'pickler': 'string(default="cPickle:Pickler")',
             'unpickler': 'string(default="cPickle:Unpickler")',
+            'serializer': 'string(default="nagare.sessions.serializer:Dummy")'
            }
 
     def __init__(
                     self,
                     states_history=True,
-                    pickler=cPickle.Pickler, unpickler=cPickle.Unpickler,
-                    security_cookie_name='_nagare'
+                    security_cookie_name='_nagare',
+                    serializer=serializer.Dummy, pickler=None, unpickler=None
                 ):
         """Initialization
 
         In:
+          - ``states_history`` -- are all the states kept or only the latest?
           - ``security_cookie_name`` -- name of the cookie where the session secure id is stored
+          - ``serializer`` -- serializer / deserializer of the states
+          - ``pickler`` -- pickler used by the serializer
+          - ``unpickler`` -- unpickler used by the serializer
         """
-        self.states_history = True
-        self.pickler = pickler
-        self.unpickler = unpickler
+        self.states_history = states_history
         self.security_cookie_name = security_cookie_name
+        self.serializer = serializer(pickler, unpickler)
 
     def set_config(self, filename, conf, error):
         """Read the configuration parameters
           - ``conf`` -- the ``ConfigObj`` object, created from the configuration file
           - ``error`` -- the function to call in case of configuration errors
         """
-        conf = dict([(k, v) for (k, v) in conf.items() if k in self.spec])
+        conf = dict([(k, v) for k, v in conf.items() if k in self.spec])
         conf = configobj.ConfigObj(conf, configspec=self.spec)
         config.validate(filename, conf, error)
 
+        self.states_history = conf['states_history']
         self.security_cookie_name = conf['security_cookie_name']
-        self.states_history = conf['states_history']
 
-        self.pickler = reference.load_object(conf['pickler'])[0]
-        self.unpickler = reference.load_object(conf['unpickler'])[0]
+        pickler = reference.load_object(conf['pickler'])[0]
+        unpickler = reference.load_object(conf['unpickler'])[0]
+        serializer = reference.load_object(conf['serializer'])[0]
+        self.serializer = serializer(pickler, unpickler)
 
         return conf
 
-    def set_persistent_id(self, pickler, persistent_id):
-        # As `inst_persistent_id` is a read-only attribute of the `cPickle.Pickler`
-        # implementation, `hasattr(pickler, 'inst_persistent')` always returns `False`
-        if 'inst_persistent_id' in dir(pickler):
-            pickler.inst_persistent_id = persistent_id
-        else:
-            pickler.persistent_id = persistent_id
-
-    def get(self, request, response, use_same_state):
-        """Create a new state or return an existing one
-
-        In:
-          - ``request`` -- the web request object
-          - ``response`` -- the web response object
-          - ``use_same_state`` -- is a copy of the state to create ?
-
-        Return:
-          - the state object
-        """
-        (session_id, state_id) = self._get_ids(request)
-        is_new = not session_id or not state_id
-
-        if is_new:
-            # New session: create a new session id
-            while True:
-                session_id = str(random.randint(1000000000000000, 9999999999999999))
-                if not self.is_session_exist(session_id):
-                    break
-
-        state = State(self, session_id, state_id, use_same_state or not self.states_history)
-
-        if self.security_cookie_name:
-            secure_id = request.cookies.get(self.security_cookie_name) or str(random.randint(1000000000000000, 9999999999999999))
-        else:
-            secure_id = None
-
-        if is_new:
-            # New state
-            # ---------
-
-            state.create(secure_id)
-
-            if self.security_cookie_name:
-                response.set_cookie(self.security_cookie_name, secure_id, path=request.script_name + '/')
-        else:
-            # Existing state
-            # --------------
-
-            session_secure_id = state.get()
-
-            if session_secure_id != secure_id:
-                raise ExpirationError()
-
-        return state
-
-    def _get_ids(self, request):
-        """Search the session id and the state id into the request parameters
-
-        In:
-          - ``request`` -- the web request object
-
-        Return:
-          - a tuple (session id, state id) or ('', '') if no session found
-        """
-        return (
-                    str(request.params.get('_s', '')),
-                    str(request.params.get('_c', '')) if self.states_history else '0'
-                )
-
     def sessionid_in_url(self, session_id, state_id, request, response):
         """Return the session and states ids to put into an URL
 
         In:
-          - ``request`` -- the web request object
-          - ``response`` -- the web response object
+          - ``request`` -- the web request
+          - ``response`` -- the web response
 
         Return:
-          - tuple (session id parameter, state id parameter)
+          - session id parameter
+          - state id parameter (optional, only if ``states_history`` is ``True``)
         """
-        ids = ('_s=' + session_id,)
+        ids = ('_s=%d' % session_id,)
         if self.states_history:
             ids += ('_c=%05d' % state_id,)
 
                     h.input(name='_c', value='%05d' % state_id, type='hidden')
                 )
 
-    def get_state(self, session_id, state_id, use_same_state):
-        """Retrieve the state
+    def _get_ids(self, request):
+        """Search the session id and the state id into the request parameters
+
+        In:
+          - ``request`` -- the web request
+
+        Return:
+          - session id
+          - state id
+        """
+        return (
+                    int(request.params['_s']),
+                    int(request.params['_c']) if self.states_history else 0
+                )
+
+    def check_session_id(self, session_id):
+        """Test if a session exist
+
+        In:
+          - ``session_id`` -- id of a session
+
+        Return:
+          - is ``session_id`` the id of an existing session?
+        """
+        return False
+
+    def get_state(self, request, response, use_same_state):
+        """Create a new state or return an existing one
+
+        In:
+          - ``request`` -- the web request object
+          - ``response`` -- the web response object
+          - ``use_same_state`` -- is a copy of the state to created?
+
+        Return:
+          - the state
+        """
+        try:
+            session_id, state_id = self._get_ids(request)
+        except (KeyError, ValueError, TypeError):
+            state_id = None
+
+            # Create a new session id
+            while True:
+                session_id = random.randint(1000000000000000, 9999999999999999)
+                if not self.check_session_id(session_id):
+                    break
+
+        secure_id = None
+        if self.security_cookie_name:
+            secure_id = request.cookies.get(self.security_cookie_name)
+            if not secure_id:
+                secure_id = str(random.randint(1000000000000000, 9999999999999999))
+                response.set_cookie(self.security_cookie_name, secure_id, path=request.script_name + '/')
+
+        return State(self, session_id, state_id, secure_id, use_same_state or not self.states_history)
+
+    def get_root(self, session_id, state_id):
+        """Retrieve the objects graph of a state
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
-          - ``use_same_state`` -- is a copy of this state to create ?
 
         Return:
-          - the tuple:
-            - id of this state,
-            - session lock,
-            - secure number associated to the session,
-            - objects graph,
-            - callbacks
+          - id of the latest state
+          - secure number associated to the session
+          - objects graph
         """
-        (state_id, lock, secure_id, session_data, state_data) = self._get(session_id, state_id, use_same_state)
-        data = self.deserialize(session_data, state_data)
-        return (state_id, lock, secure_id, data)
+        new_state_id, secure_id, session_data, state_data = self.fetch_state(session_id, state_id)
+        return new_state_id, secure_id, self.serializer.loads(session_data, state_data)
 
-    def create_state(self, session_id, secure_id):
-        """Initialized a new state, with an empty objects graph
-
-        In:
-          - ``session_id`` -- session id of this state
-          - ``secure_id`` -- the secure number associated to the session
-
-        Return:
-          - the tuple:
-            - id of this state,
-            - session lock
-        """
-        return self._create(session_id, secure_id)
-
-    def set(self, state, use_same_state):
-        """Store the state
-
-        In:
-          - ``state`` -- the state object
-          - ``use_same_state`` -- is a copy of this state to create ?
-        """
-        state.set(self, use_same_state)
-
-    def set_state(self, session_id, state_id, secure_id, use_same_state, data):
+    def set_root(self, session_id, state_id, secure_id, use_same_state, data):
         """Store the state
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
           - ``secure_id`` -- the secure number associated to the session
-          - ``use_same_state`` -- is this state to be stored in the previous snapshot ?
+          - ``use_same_state`` -- is a copy of this state to be created?
           - ``data`` -- the objects graph
         """
-        self._set(session_id, state_id, secure_id, use_same_state, *self.serialize(data, not use_same_state))
+        session_data, state_data = self.serializer.dumps(data, not use_same_state)
+        self.store_state(session_id, state_id, secure_id, use_same_state, session_data, state_data)
+
+    # -------------------------------------------------------------------------
+
+    def create_lock(self, session_id):
+        """Create a new lock for a session
+
+        In:
+          - ``session_id`` -- session id
+
+        Return:
+          - the lock
+        """
+        return self.get_lock(session_id)
+
+    def get_lock(self, session_id):
+        """Retrieve the lock of a session
+
+        In:
+          - ``session_id`` -- session id
+
+        Return:
+          - the lock
+        """
+        raise NotImplementedError()
+
+    def create(self, session_id, secure_id, lock):
+        """Create a new session
+
+        In:
+          - ``session_id`` -- id of the session
+          - ``secure_id`` -- the secure number associated to the session
+          - ``lock`` -- the lock of the session
+        """
+        raise NotImplementedError()
 
     def delete(self, session_id):
-        """Delete the session
+        """Delete a session
+
+        In:
+          - ``session_id`` -- id of the session to delete
+        """
+        raise NotImplementedError()
+
+    def fetch_state(self, session_id, state_id):
+        """Retrieve a state with its associated objects graph
 
         In:
           - ``session_id`` -- session id of this state
+          - ``state_id`` -- id of this state
+
+        Return:
+          - id of the more recent stored state
+          - secure number associated to the session
+          - data kept into the session
+          - data kept into the state
         """
-        self._delete(session_id)
+        raise NotImplementedError()
 
-    def is_session_exist(self, session_id):
-        """Test if a session id is invalid
+    def store_state(self, session_id, state_id, secure_id, use_same_state, session_data, state_data):
+        """Store a state and its associated objects graph
 
         In:
-          - ``session_id`` -- id of the session
-
-        Return:
-          - a boolean
+          - ``session_id`` -- session id of this state
+          - ``state_id`` -- id of this state
+          - ``secure_id`` -- the secure number associated to the session
+          - ``use_same_state`` -- is this state to be stored in the previous snapshot?
+          - ``session_data`` -- data to keep into the session
+          - ``state_data`` -- data to keep into the state
         """
-        return False
-
-    def pickle(self, data, clean_callbacks):
-        """Pickle an objects graph
-
-        In:
-          - ``data`` -- the objects graph
-          - ``clean_callbacks`` -- do we have to forget the old callbacks?
-
-        Out:
-          - the tuple:
-            - data to keep into the session
-            - data to keep into the state
-        """
-        f = cStringIO.StringIO()
-        p = self.pickler(f, protocol=-1)
-
-        session_data = {}
-        tasklets = set()
-        callbacks = {}
-
-        # Serialize the objects graph and extract all the callbacks
-        self.set_persistent_id(p, lambda o: persistent_id(o, clean_callbacks, callbacks, session_data, tasklets))
-        p.dump(data)
-
-        # Serialize the callbacks
-        self.set_persistent_id(p, lambda o: None)
-        p.dump(callbacks)
-
-        # Kill all the blocked tasklets, which are now serialized
-        for t in tasklets:
-            t.kill()
-
-        return (session_data, f.getvalue())
-
-    def unpickle(self, session_data, state_data):
-        """Unpickle an objects graph
-
-        In:
-          - ``session_data`` -- data from the session
-          - ``state_data`` -- data from the state
-
-        Out:
-          - tuple (the objects graph, the callbacks)
-        """
-        p = self.unpickler(cStringIO.StringIO(state_data))
-        if session_data:
-            p.persistent_load = lambda i: session_data.get(int(i))
-
-        return p.load(), p.load()
+        raise NotImplementedError()

nagare/sessions/memcached_sessions.py

 
 from nagare import local
 from nagare.sessions import ExpirationError, common
+from nagare.sessions.serializer import Pickle
 
-KEY_PREFIX = 'nagare_'
+KEY_PREFIX = 'nagare_%d_'
 
 
 class Lock(object):
           - ``max_wait_time`` -- maximum time to wait to acquire the lock, in seconds
         """
         self.connection = connection
-        self.lock = '%slock_%s' % (KEY_PREFIX, lock_id)
+        self.lock = (KEY_PREFIX + 'lock') % lock_id
         self.ttl = ttl
         self.poll_time = poll_time
         self.max_wait_time = max_wait_time
 class Sessions(common.Sessions):
     """Sessions manager for sessions kept in an external memcached server
     """
-    spec = dict(
+    spec = common.Sessions.spec.copy()
+    spec.update(dict(
                 host='string(default="127.0.0.1")',
                 port='integer(default=11211)',
                 ttl='integer(default=0)',
                 min_compress_len='integer(default=0)',
                 reset='boolean(default=True)',
                 debug='boolean(default=False)',
-               )
-    spec.update(common.Sessions.spec)
+                serializer='string(default="nagare.sessions.serializer:Pickle")'
+               ))
 
     def __init__(
                  self,
                  min_compress_len=0,
                  reset=False,
                  debug=True,
+                 serializer=None,
                  **kw
                 ):
         """Initialization
           - ``min_compress_len`` -- data longer than this value are sent compressed
           - ``reset`` -- do a reset of all the sessions on startup ?
           - ``debug`` -- display the memcache requests / responses
+          - ``serializer`` -- serializer / deserializer of the states
         """
-        super(Sessions, self).__init__(**kw)
+        super(Sessions, self).__init__(serializer or Pickle, **kw)
 
         self.host = ['%s:%d' % (host, port)]
         self.ttl = ttl
         memcached = memcache.Client(self.host, debug=self.debug)
         memcached.flush_all()
 
-    def _create(self, session_id, secure_id):
+    def get_lock(self, session_id):
+        """Retrieve the lock of a session
+
+        In:
+          - ``session_id`` -- session id
+
+        Return:
+          - the lock
+        """
+        connection = self._get_connection()
+        return Lock(connection, session_id, self.lock_ttl, self.lock_poll_time, self.lock_max_wait_time)
+
+    def create(self, session_id, secure_id, lock):
         """Create a new session
 
         In:
           - ``session_id`` -- id of the session
           - ``secure_id`` -- the secure number associated to the session
+          - ``lock`` -- the lock of the session
+        """
+        self._get_connection().set_multi({
+            'state': 0,
+            'sess': (secure_id, None),
+            '00000': {}
+        }, self.ttl, KEY_PREFIX % session_id, self.min_compress_len)
 
-        Return:
-          - the tuple:
-            - id of this state,
-            - session lock
+    def delete(self, session_id):
+        """Delete a session
+
+        In:
+          - ``session_id`` -- id of the session to delete
         """
-        connection = self._get_connection()
-        lock = Lock(connection, session_id, self.lock_ttl, self.lock_poll_time, self.lock_max_wait_time)
-        lock.acquire()
+        self._get_connection().delete((KEY_PREFIX + 'sess') % session_id)
 
-        connection.set_multi({
-            '_sess': (secure_id, None),
-            '_state': '0',
-            '00000': {}
-        }, self.ttl, KEY_PREFIX + session_id, self.min_compress_len)
-
-        return (0, lock)
-
-    def _get(self, session_id, state_id, use_same_state):
-        """Retrieve the state
+    def fetch_state(self, session_id, state_id):
+        """Retrieve a state with its associated objects graph
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
-          - ``use_same_state`` -- is a copy of this state to create ?
 
         Return:
-          - the tuple:
-            - id of this state,
-            - session lock,
-            - secure number associated to the session,
-            - data keept into the session
-            - data keept into the state
+          - id of the latest state
+          - secure number associated to the session
+          - data kept into the session
+          - data kept into the state
         """
-        connection = self._get_connection()
-        lock = Lock(connection, session_id, self.lock_ttl, self.lock_poll_time, self.lock_max_wait_time)
-        lock.acquire()
-
-        state_id = state_id.zfill(5)
-        session = connection.get_multi(('_sess', '_state', state_id), KEY_PREFIX + session_id)
+        state_id = '%05d' % state_id
+        session = self._get_connection().get_multi(('state', 'sess', state_id), KEY_PREFIX % session_id)
 
         if len(session) != 3:
             raise ExpirationError()
 
-        (secure_id, session_data) = session['_sess']
-        last_state_id = int(session['_state'])
+        last_state_id = session['state']
+        secure_id, session_data = session['sess']
         state_data = session[state_id]
 
-        if not use_same_state:
-            state_id = last_state_id
+        return last_state_id, secure_id, session_data, state_data
 
-        return (int(state_id), lock, secure_id, session_data, state_data)
-
-    def _set(self, session_id, state_id, secure_id, use_same_state, session_data, state_data):
-        """Store the state
+    def store_state(self, session_id, state_id, secure_id, use_same_state, session_data, state_data):
+        """Store a state and its associated objects graph
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
           - ``secure_id`` -- the secure number associated to the session
-          - ``use_same_state`` -- is this state to be stored in the previous snapshot ?
-          - ``session_data`` -- data keept into the session
-          - ``state_data`` -- data keept into the state
+          - ``use_same_state`` -- is this state to be stored in the previous snapshot?
+          - ``session_data`` -- data to keep into the session
+          - ``state_data`` -- data to keep into the state
         """
         if not use_same_state:
-            self._get_connection().incr(KEY_PREFIX + session_id + '_state')
+            self._get_connection().incr((KEY_PREFIX + 'state') % session_id)
 
         self._get_connection().set_multi({
-            '_sess': (secure_id, session_data),
+            'sess': (secure_id, session_data),
             '%05d' % state_id: state_data
-        }, self.ttl, KEY_PREFIX + session_id, self.min_compress_len)
-
-    def _delete(self, session_id):
-        """Delete the session
-
-        In:
-          - ``session_id`` -- id of the session to delete
-        """
-        self._get_connection().delete(KEY_PREFIX + session_id)
-
-    def serialize(self, data, clean_callbacks):
-        """Pickle an objects graph
-
-        In:
-          - ``data`` -- the objects graphs
-          - ``clean_callbacks`` -- do we have to forget the old callbacks?
-
-        Return:
-          - the tuple:
-            - data to keep into the session
-            - data to keep into the state
-        """
-        return self.pickle(data, clean_callbacks)
-
-    def deserialize(self, session_data, state_data):
-        """Unpickle an objects graph
-
-        In:
-          - ``session_data`` -- data from the session
-          - ``state_data`` -- data from the state
-
-        Out:
-          - tuple (the objects graph, the callbacks)
-        """
-        return self.unpickle(session_data, state_data)
+        }, self.ttl, KEY_PREFIX % session_id, self.min_compress_len)

nagare/sessions/memory_sessions.py

 
 """Sessions managed in memory
 
-These sessions managers keeps:
+These sessions managers keep:
   - the last recently used ``DEFAULT_NB_SESSIONS`` sessions
   - for each session, the last recently used ``DEFAULT_NB_STATES`` states
 """
 
 from nagare import local
 from nagare.sessions import ExpirationError, common, lru_dict
+from nagare.sessions.serializer import Pickle
 
 DEFAULT_NB_SESSIONS = 10000
 DEFAULT_NB_STATES = 20
 
 
-class SessionsBase(common.Sessions):
-    """Sessions manager for sessions kept in memory
+class Sessions(common.Sessions):
+    """Sessions manager for states kept in memory
     """
-    spec = {
-            'nb_sessions': 'integer(default=%d)' % DEFAULT_NB_SESSIONS,
-            'nb_states': 'integer(default=%d)' % DEFAULT_NB_STATES
-           }
-
-    spec.update(common.Sessions.spec)
+    spec = common.Sessions.spec.copy()
+    spec['nb_sessions'] = 'integer(default=%d)' % DEFAULT_NB_SESSIONS
+    spec['nb_states'] = 'integer(default=%d)' % DEFAULT_NB_STATES
 
     def __init__(self, nb_sessions=DEFAULT_NB_SESSIONS, nb_states=DEFAULT_NB_STATES, **kw):
         """Initialization
           - ``nb_sessions`` -- maximum number of sessions kept in memory
           - ``nb_states`` -- maximum number of states, for each sessions, kept in memory
         """
-        super(SessionsBase, self).__init__(**kw)
+        super(Sessions, self).__init__(**kw)
 
         self.nb_states = nb_states
         self._sessions = lru_dict.ThreadSafeLRUDict(nb_sessions)
         """Read the configuration parameters
 
         In:
-          - ``filename`` -- the path to the configuration file
-          - ``conf`` -- the ``ConfigObj`` object, created from the configuration file
-          - ``error`` -- the function to call in case of configuration errors
+          - ``filename`` -- path to the configuration file
+          - ``conf`` -- ``ConfigObj`` object created from the configuration file
+          - ``error`` -- function to call in case of configuration errors
         """
         # Let's the super class validate the configuration file
-        conf = super(SessionsBase, self).set_config(filename, conf, error)
+        conf = super(Sessions, self).set_config(filename, conf, error)
 
         self.nb_states = conf['nb_states']
         self._sessions = lru_dict.ThreadSafeLRUDict(conf['nb_sessions'])
 
         return conf
 
-    def is_session_exist(self, session_id):
-        """Test if a session id is invalid
+    def check_session_id(self, session_id):
+        """Test if a session exist
 
         In:
-          - ``session_id`` -- id of the session
+          - ``session_id`` -- id of a session
 
         Return:
-          - a boolean
+          - is ``session_id`` the id of an existing session?
         """
         return session_id in self._sessions
 
-    def _create(self, session_id, secure_id):
+    def create_lock(self, session_id):
+        """Create a new lock for a session
+
+        In:
+          - ``session_id`` -- session id
+
+        Return:
+          - the lock
+        """
+        return local.worker.create_lock()
+
+    def get_lock(self, session_id):
+        """Retrieve the lock of a session
+
+        In:
+          - ``session_id`` -- session id
+
+        Return:
+          - the lock
+        """
+        try:
+            return self._sessions[session_id][1]
+        except KeyError:
+            raise ExpirationError()
+
+    def create(self, session_id, secure_id, lock):
         """Create a new session
 
         In:
           - ``session_id`` -- id of the session
           - ``secure_id`` -- the secure number associated to the session
+          - ``lock`` -- the lock of the session
+        """
+        self._sessions[session_id] = [0, lock, secure_id, None, lru_dict.LRUDict(self.nb_states)]
 
-        Return:
-          - the tuple:
-            - id of this state,
-            - session lock
+    def delete(self, session_id):
+        """Delete a session
+
+        In:
+          - ``session_id`` -- id of the session to delete
         """
-        lock = local.worker.create_lock()
-        lock.acquire()
+        del self._sessions[session_id]
 
-        self._sessions[session_id] = [0, lock, secure_id, None, lru_dict.LRUDict(self.nb_states)]
-        return (0, lock)
-
-    def _get(self, session_id, state_id, use_same_state):
-        """Retrieve the state
+    def fetch_state(self, session_id, state_id):
+        """Retrieve a state with its associated objects graph
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
-          - ``use_same_state`` -- is a copy of this state to create ?
 
         Return:
-          - the tuple:
-            - id of this state,
-            - session lock,
-            - secure number associated to the session,
-            - data keept into the session
-            - data keept into the state
+          - id of the latest state
+          - secure number associated to the session
+          - data kept into the session
+          - data kept into the state
         """
         try:
-            lock = self._sessions[session_id][1]
+            last_state_id, _, secure_id, session_data, states = self._sessions[session_id]
+            state_data = states[state_id]
         except KeyError:
             raise ExpirationError()
 
-        lock.acquire()
+        return last_state_id, secure_id, session_data, state_data
 
-        try:
-            state_id = int(state_id)
-            (last_state_id, lock, secure_id, session_data, states) = self._sessions[session_id]
-            state_data = states[state_id]
-        except (KeyError, ValueError, TypeError):
-            raise ExpirationError()
-
-        if not use_same_state:
-            state_id = last_state_id
-
-        return (state_id, lock, secure_id, session_data, state_data)
-
-    def _set(self, session_id, state_id, secure_id, use_same_state, session_data, state_data):
-        """Store the state
+    def store_state(self, session_id, state_id, secure_id, use_same_state, session_data, state_data):
+        """Store a state and its associated objects graph
 
         In:
           - ``session_id`` -- session id of this state
           - ``state_id`` -- id of this state
           - ``secure_id`` -- the secure number associated to the session
-          - ``use_same_state`` -- is this state to be stored in the previous snapshot ?
-          - ``session_data`` -- data keept into the session
-          - ``state_data`` -- data keept into the state
+          - ``use_same_state`` -- is this state to be stored in the previous snapshot?
+          - ``session_data`` -- data to keep into the session
+          - ``state_data`` -- data to keep into the state
         """
         session = self._sessions[session_id]
 
         session[3] = session_data
         session[4][state_id] = state_data
 
-    def _delete(self, session_id):
-        """Delete the session
+
+class SessionsWithPickledStates(Sessions):
+    """Sessions manager for states pickled / unpickled in memory
+    """
+    spec = Sessions.spec.copy()
+    spec['serializer'] = 'string(default="nagare.sessions.serializer:Pickle")'
+
+    def __init__(self, serializer=None, **kw):
+        """Initialization
 
         In:
-          - ``session_id`` -- id of the session to delete
+          - ``serializer`` -- serializer / deserializer of the states
         """
-        del self._sessions[session_id]
-
-
-class SessionsWithPickledStates(SessionsBase):
-    """Sessions managers that pickle / unpickle the objects graph
-    """
-    def serialize(self, data, clean_callbacks):
-        """Pickle an objects graph
-
-        In:
-          - ``data`` -- the objects graphs
-          - ``clean_callbacks`` -- do we have to forget the old callbacks?
-
-        Return:
-          - the tuple:
-            - data to keep into the session
-            - data to keep into the state
-        """
-        return self.pickle(data, clean_callbacks)
-
-    def deserialize(self, session_data, state_data):
-        """Unpickle an objects graph
-
-        In:
-          - ``session_data`` -- data from the session
-          - ``state_data`` -- data from the state
-
-        Out:
-          - tuple (the objects graph, the callbacks)
-        """
-        return self.unpickle(session_data, state_data)
+        super(Sessions, self).__init__(serializer=serializer or Pickle, **kw)

nagare/sessions/serializer.py

+#--
+# Copyright (c) 2008-2013 Net-ng.
+# All rights reserved.
+#
+# This software is licensed under the BSD License, as described in
+# the file LICENSE.txt, which you should have received as part of
+# this distribution.
+#--
+
+import cStringIO
+import cPickle
+
+from nagare.continuation import Tasklet
+from nagare.component import Component
+
+
+def persistent_id(o, clean_callbacks, callbacks, session_data, tasklets):
+    """An object with a ``_persistent_id`` attribute is stored into the session
+    not into the state snapshot
+
+    In:
+      - ``o`` -- object to check
+      - ``clean_callbacks`` -- do we have to forget the old callbacks?
+
+    Out:
+      - ``callbacks`` -- merge of the callbacks from all the components
+      - ``session_data`` -- dict persistent_id -> object of the objects to store into the session
+      - ``tasklets`` -- set of the serialized tasklets
+
+    Return:
+      - the persistent id or ``None``
+    """
+    r = None
+
+    id_ = getattr(o, '_persistent_id', None)
+    if id_ is not None:
+        session_data[id_] = o
+        r = str(id_)
+
+    elif type(o) is Tasklet:
+        tasklets.add(o)
+
+    elif isinstance(o, Component):
+        callbacks.update(o.serialize_callbacks(clean_callbacks))
+
+    return r
+
+
+def set_persistent_id(pickler, persistent_id):
+    if 'inst_persistent_id' in dir(pickler):
+        pickler.inst_persistent_id = persistent_id
+    else:
+        pickler.persistent_id = persistent_id
+
+
+class DummyFile(object):
+    """A write-only file that does nothing"""
+    def write(self, data):
+        pass
+
+
+class Dummy(object):
+    def __init__(self, pickler=None, unpickler=None):
+        """Initialization
+
+          - ``pickler`` -- pickler to use
+          - ``unpickler`` -- unpickler to use
+        """
+        self.pickler = pickler or cPickle.Pickler
+        self.unpickler = unpickler or cPickle.Unpickler
+
+    def _dumps(self, pickler, data, clean_callbacks):
+        """Serialize an objects graph
+
+        In:
+          - ``pickler`` -- pickler to use
+          - ``data`` -- the objects graph
+          - ``clean_callbacks`` -- do we have to forget the old callbacks?
+
+        Out:
+          - data to keep into the session
+          - data to keep into the state
+        """
+        session_data = {}
+        tasklets = set()
+        callbacks = {}
+
+        # Serialize the objects graph and extract all the callbacks
+        set_persistent_id(pickler, lambda o: persistent_id(o, clean_callbacks, callbacks, session_data, tasklets))
+        pickler.dump(data)
+
+        return session_data, callbacks, tasklets
+
+    def dumps(self, data, clean_callbacks):
+        """Serialize an objects graph
+
+        In:
+          - ``data`` -- the objects graph
+          - ``clean_callbacks`` -- do we have to forget the old callbacks?
+
+        Out:
+          - data kept into the session
+          - data kept into the state
+        """
+        pickler = self.pickler(DummyFile(), protocol=-1)
+        session_data, callbacks, tasklets = self._dumps(pickler, data, clean_callbacks)
+
+        # This dummy serializer returns the data untouched
+        return None, (data, callbacks)
+
+    def loads(self, session_data, state_data):
+        """Deserialize an objects graph
+
+        In:
+          - ``session_data`` -- data from the session
+          - ``state_data`` -- data from the state
+
+        Out:
+          - the objects graph
+          - the callbacks
+        """
+        return state_data
+
+
+class Pickle(Dummy):
+    def dumps(self, data, clean_callbacks):
+        """Serialize an objects graph
+
+        In:
+          - ``data`` -- the objects graph
+          - ``clean_callbacks`` -- do we have to forget the old callbacks?
+
+        Out:
+          - data kept into the session
+          - data kept into the state
+        """
+        f = cStringIO.StringIO()
+        pickler = self.pickler(f, protocol=-1)
+        # Pickle the data
+        session_data, callbacks, tasklets = self._dumps(pickler, data, clean_callbacks)
+
+        # Pickle the callbacks
+        set_persistent_id(pickler, lambda o: None)
+        pickler.dump(callbacks)
+
+        # Kill all the blocked tasklets, which are now serialized
+        for t in tasklets:
+            t.kill()
+
+        # The pickled data are returned
+        return (session_data, f.getvalue())
+
+    def loads(self, session_data, state_data):
+        """Deserialize an objects graph
+
+        In:
+          - ``session_data`` -- data from the session
+          - ``state_data`` -- data from the state
+
+        Out:
+          - the objects graph
+          - the callbacks
+        """
+        p = self.unpickler(cStringIO.StringIO(state_data))
+        if session_data:
+            p.persistent_load = lambda i: session_data.get(int(i))
+
+        return p.load(), p.load()
       standalone = nagare.sessions.memory_sessions:SessionsWithPickledStates
       pickle = nagare.sessions.memory_sessions:SessionsWithPickledStates
       memcache = nagare.sessions.memcached_sessions:Sessions
-      dummy = nagare.sessions.dummy_sessions:Sessions
 
       [nagare.applications]
       admin = nagare.admin.admin_app:app