Robert Brewer avatar Robert Brewer committed 775abbd

Revamped session module. Much better tests. Cleanup is now in a separate, cycling Timer thread (with an entry in on_stop_engine_list). Moved cherrypy.request._session to cherrypy.serving.session.

Comments (0)

Files changed (6)

         d.update(childobject.__dict__)
         return d
     __dict__ = property(_get_dict)
+    
+    def __getitem__(self, key):
+        childobject = getattr(serving, self.__attrname__)
+        return childobject[key]
+    
+    def __setitem__(self, key, value):
+        childobject = getattr(serving, self.__attrname__)
+        childobject[key] = value
 
 
 # Create request and response object (the same objects will be used
 class SessionTool(Tool):
     """Session Tool for CherryPy."""
     
-    def __init__(self):
-        self._point = "before_finalize"
-        self.callable = _sessions.save
-        self._name = None
-        for k in dir(_sessions.Session):
-            if k not in ("init", "save") and not k.startswith("__"):
-                setattr(self, k, None)
-    
-    def _init(self):
-        conf = cherrypy.request.toolmap.get(self._name, {})
-        
-        s = cherrypy.request._session = _sessions.Session()
-        # Copy all conf entries onto Session object attributes
-        for k, v in conf.iteritems():
-            setattr(s, str(k), v)
-        s.init()
-        
-        if not hasattr(cherrypy, "session"):
-            cherrypy.session = _sessions.SessionWrapper()
-    
     def _setup(self):
         """Hook this tool into cherrypy.request using the given conf.
         
         The standard CherryPy request object will automatically call this
         method when the tool is "turned on" in config.
         """
-        # init must be bound after headers are read
-        cherrypy.request.hooks.attach('before_request_body', self._init)
+        Tool._setup(self)
         cherrypy.request.hooks.attach('before_finalize', _sessions.save)
-        cherrypy.request.hooks.attach('on_end_request', _sessions.cleanup)
+        cherrypy.request.hooks.attach('on_end_request', _sessions.close)
 
 
 class XMLRPCController(object):
 default_toolbox.gzip = Tool('before_finalize', encoding.gzip)
 default_toolbox.staticdir = MainTool(static.staticdir)
 default_toolbox.staticfile = MainTool(static.staticfile)
-default_toolbox.sessions = SessionTool()
+# _sessions.init must be bound after headers are read
+default_toolbox.sessions = SessionTool('before_request_body', _sessions.init)
 default_toolbox.xmlrpc = XMLRPCTool()
 default_toolbox.wsgiapp = WSGIAppTool(_wsgiapp.run)
 default_toolbox.caching = CachingTool()
     
     request = cherrypy.request
     tdata = cherrypy.thread_data
-    sess = getattr(cherrypy, "session", None)
-    if sess is None:
-        # Shouldn't this raise an error (if the sessions tool isn't enabled)?
-        return False
-    
+    sess = cherrypy.session
     request.user = None
     tdata.user = None
     
-##    conf = cherrypy.config.get
-##    if conf('tools.staticfile.on', False) or conf('tools.staticdir.on', False):
-##        return
     if request.path.endswith('login_screen'):
         return False
     elif request.path.endswith('do_logout'):
 We use cherrypy.request to store some convenient variables as
 well as data about the session for the current request. Instead of
 polluting cherrypy.request we use a Session object bound to
-cherrypy.request._session to store these variables.
-
-Global variables (RAM backend only):
-    - _session_lock_dict: dictionary containing the locks for all session_id
-    - _session_data_holder: dictionary containing the data for all sessions
-
+cherrypy.session to store these variables.
 """
 
 import datetime
 import random
 import sha
 import time
-import thread
 import threading
 import types
 
 import cherrypy
 from cherrypy.lib import http
 
-_session_last_clean_up_time = datetime.datetime.now()
-_session_data_holder = {} # Needed for RAM sessions only
-_session_lock_dict = {} # Needed for RAM sessions only
 
-
-def generate_id(self=None):
-    """Return a new session id"""
-    return sha.new('%s' % random.random()).hexdigest()
-
-
-def noop(self, data=None): pass
-
-class Session:
-    """A CherryPy Session object (one per request).
+class Session(object):
+    """A CherryPy dict-like Session object (one per request).
     
-    timeout: timeout delay for the session
-    locking: mechanism used to lock the session ('implicit' or 'explicit')
-    storage (instance of the class implementing the backend)
-    data: dictionary containing the actual session data
-    id: current session ID
-    expiration_time: date/time when the current session will expire
+    id: current session ID.
+    expiration_time (datetime): when the current session will expire.
+    timeout (minutes): used to calculate expiration_time from now.
+    clean_freq (minutes): the poll rate for expired session cleanup.
+    locked: If True, this session instance has exclusive read/write access
+        to session data.
+    loaded: If True, data has been retrieved from storage. This should
+        happen automatically on the first attempt to access session data.
     """
     
-    # It's important that the following are class variables,
-    # so that the CherryPy SessionTool can grab them and fake tooltips.
-    timeout = 60
-    locking = 'explicit'
-    deadlock_timeout = 30
-    clean_up_delay = 5
-    storage_type = 'Ram'
-    storage_class = None
-    cookie_name = 'session_id'
-    cookie_domain = None
-    cookie_secure = False
-    cookie_path = None
-    cookie_path_from_header = None
-    generate_id = generate_id
-    on_create = noop
-    on_renew = noop
-    on_delete = noop
+    clean_thread = None
     
-    def __init__(self):
-        self.storage = None
+    def __init__(self, id=None):
         self.locked = False
         self.loaded = False
-        self.saved = False
+        self._data = {}
         
-        self.generate_id = generate_id
-        self.on_create = noop
-        self.on_renew = noop
-        self.on_delete = noop
+        if id is None:
+            id = self.generate_id()
+        self.id = id
     
-    def init(self):
-        # People can set their own custom class
-        #   through tools.sessions.storage_class
-        if self.storage_class is None:
-            self.storage_class = globals()[self.storage_type.title() + 'Storage']
-        self.storage = self.storage_class()
-        
-        now = datetime.datetime.now()
-        # Check if we need to clean up old sessions
-        global _session_last_clean_up_time
-        clean_up_delay = datetime.timedelta(seconds = self.clean_up_delay * 60)
-        if _session_last_clean_up_time + clean_up_delay < now:
-            _session_last_clean_up_time = now
-            # Run clean_up in other thread to avoid blocking this request
-            thread.start_new_thread(self.storage.clean_up, (self,))
-        
-        self.data = {}
-        
-        if self.cookie_path is None:
-            if self.cookie_path_from_header is not None:
-                geth = cherrypy.request.headers.get
-                self.cookie_path = geth(self.cookie_path_from_header, None)
-            if self.cookie_path is None:
-                self.cookie_path = '/'
-        
-        # Check if request came with a session ID
-        if self.cookie_name in cherrypy.request.simple_cookie:
-            # It did: we mark the data as needing to be loaded
-            self.id = cherrypy.request.simple_cookie[self.cookie_name].value
-            
-            # If using implicit locking, acquire lock
-            if self.locking == 'implicit':
-                self.data['_id'] = self.id
-                self.storage.acquire_lock()
-        else:
-            # No id yet
-            self.id = self.generate_id()
-            self.data['_id'] =  self.id
-            self.on_create(self.data)
-        
-        # Set response cookie
-        cookie = cherrypy.response.simple_cookie
-        cookie[self.cookie_name] = self.id
-        cookie[self.cookie_name]['path'] = self.cookie_path
-        # We'd like to use the "max-age" param as
-        #   http://www.faqs.org/rfcs/rfc2109.html indicates but IE doesn't
-        #   save it to disk and the session is lost if people close
-        #   the browser
-        #   So we have to use the old "expires" ... sigh ...
-        #cookie[cookie_name]['max-age'] = self.timeout * 60
-        if self.timeout:
-            expiry = time.time() + (self.timeout * 60)
-            cookie[self.cookie_name]['expires'] = http.HTTPDate(expiry)
-        if self.cookie_domain is not None:
-            cookie[self.cookie_name]['domain'] = self.cookie_domain
-        if self.cookie_secure is True:
-            cookie[self.cookie_name]['secure'] = 1
+    def clean_cycle(self):
+        """Clean up expired sessions at regular intervals."""
+        # clean_thread is both a cancelable Timer and a flag.
+        if self.clean_thread:
+            self.clean_up()
+            t = threading.Timer(self.clean_freq, self.clean_cycle)
+            self.__class__.clean_thread = t
+            t.start()
+    
+    def clean_interrupt(cls):
+        """Stop the expired-session cleaning cycle."""
+        if cls.clean_thread:
+            cls.clean_thread.cancel()
+            cls.clean_thread = None
+    clean_interrupt = classmethod(clean_interrupt)
+    
+    def clean_up(self):
+        """Clean up expired sessions."""
+        pass
+    
+    def generate_id(self):
+        """Return a new session id."""
+        return sha.new('%s' % random.random()).hexdigest()
     
     def save(self):
-        """Save session data"""
+        """Save session data."""
         # If session data has never been loaded then it's never been
         #   accessed: no need to delete it
         if self.loaded:
             t = datetime.timedelta(seconds = self.timeout * 60)
             expiration_time = datetime.datetime.now() + t
-            self.storage.save(self.id, self.data, expiration_time)
+            self._save(expiration_time)
         
         if self.locked:
             # Always release the lock if the user didn't release it
-            self.storage.release_lock()
+            self.release_lock()
+    
+    def load(self):
+        """Copy stored session data into this session instance."""
+        data = self._load()
+        # data is either None or a tuple (session_data, expiration_time)
+        if data is None or data[1] < datetime.datetime.now():
+            # Expired session: flush session data (but keep the same id)
+            self._data = {}
+        else:
+            self._data = data[0]
+        self.loaded = True
         
-        self.saved = True
+        cls = self.__class__
+        if not cls.clean_thread:
+            cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt)
+            # Use the instance to call clean_cycle so tool config
+            # can be accessed inside the method.
+            cls.clean_thread = t = threading.Timer(self.clean_freq,
+                                                   self.clean_cycle)
+            t.start()
+    
+    def __getitem__(self, key):
+        if not self.loaded: self.load()
+        return self._data[key]
+    
+    def __setitem__(self, key, value):
+        if not self.loaded: self.load()
+        self._data[key] = value
+    
+    def __delitem__(self, key):
+        if not self.loaded: self.load()
+        del self._data[key]
+    
+    def __contains__(self, key):
+        if not self.loaded: self.load()
+        return key in self._data
+    
+    def has_key(self, key):
+        if not self.loaded: self.load()
+        return self._data.has_key(key)
+    
+    def get(self, key, default=None):
+        if not self.loaded: self.load()
+        return self._data.get(key, default)
+    
+    def update(self, d):
+        if not self.loaded: self.load()
+        self._data.update(d)
+    
+    def setdefault(self, key, default=None):
+        if not self.loaded: self.load()
+        return self._data.setdefault(key, default)
+    
+    def clear(self):
+        if not self.loaded: self.load()
+        self._data.clear()
+    
+    def keys(self):
+        if not self.loaded: self.load()
+        return self._data.keys()
+    
+    def items(self):
+        if not self.loaded: self.load()
+        return self._data.items()
+    
+    def values(self):
+        if not self.loaded: self.load()
+        return self._data.values()
 
 
-class SessionDeadlockError(Exception):
-    """The session could not acquire a lock after a certain time"""
-    pass
-
-
-class SessionNotEnabledError(Exception):
-    """User forgot to set tools.sessions.on to True"""
-    pass
-
-class SessionStoragePathError(Exception):
-    """User set storage_type to file but forgot to set the storage_path"""
-    pass
-
-
-class RamStorage:
-    """ Implementation of the RAM backend for sessions """
+class RamSession(Session):
     
-    def load(self, id):
-        return _session_data_holder.get(id)
+    # Class-level objects. Don't rebind these!
+    cache = {}
+    locks = {}
     
-    def save(self, id, data, expiration_time):
-        _session_data_holder[id] = (data, expiration_time)
+    def clean_up(self):
+        """Clean up expired sessions."""
+        now = datetime.datetime.now()
+        for id, (data, expiration_time) in self.cache.items():
+            if expiration_time < now:
+                try:
+                    del self.cache[id]
+                except KeyError:
+                    pass
+    
+    def _load(self):
+        return self.cache.get(self.id)
+    
+    def _save(self, expiration_time):
+        self.cache[self.id] = (self._data, expiration_time)
     
     def acquire_lock(self):
-        sess = cherrypy.request._session
-        id = cherrypy.session.id
-        lock = _session_lock_dict.get(id)
-        if lock is None:
-            lock = threading.Lock()
-            _session_lock_dict[id] = lock
-        startTime = time.time()
-        while True:
-            if lock.acquire(False):
-                break
-            if time.time() - startTime > sess.deadlock_timeout:
-                raise SessionDeadlockError()
-            time.sleep(0.5)
-        sess.locked = True
+        self.locked = True
+        self.locks.setdefault(self.id, threading.Semaphore()).acquire()
     
     def release_lock(self):
-        _session_lock_dict[cherrypy.session['_id']].release()
-        cherrypy.request._session.locked = False
-    
-    def clean_up(self, sess):
-        to_be_deleted = []
-        now = datetime.datetime.now()
-        for id, (data, expiration_time) in _session_data_holder.iteritems():
-            if expiration_time < now:
-                to_be_deleted.append(id)
-        for id in to_be_deleted:
-            try:
-                deleted_session = _session_data_holder[id]
-                del _session_data_holder[id]
-                sess.on_delete(deleted_session)
-            except KeyError:
-                # The session probably got deleted by a concurrent thread
-                #   Safe to ignore this case
-                pass
+        self.locks[self.id].release()
+        self.locked = False
 
 
-class FileStorage:
+class FileSession(Session):
     """ Implementation of the File backend for sessions """
     
     SESSION_PREFIX = 'session-'
     LOCK_SUFFIX = '.lock'
     
-    def load(self, id):
-        file_path = self._get_file_path(id)
+    def _get_file_path(self):
+        return os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
+    
+    def _load(self, path=None):
+        if path is None:
+            path = self._get_file_path()
         try:
-            f = open(file_path, "rb")
-            data = pickle.load(f)
-            f.close()
-            return data
+            f = open(path, "rb")
+            try:
+                return pickle.load(f)
+            finally:
+                f.close()
         except (IOError, EOFError):
             return None
     
-    def save(self, id, data, expiration_time):
-        file_path = self._get_file_path(id)
-        f = open(file_path, "wb")
-        pickle.dump((data, expiration_time), f)
-        f.close()
+    def _save(self, expiration_time):
+        f = open(self._get_file_path(), "wb")
+        try:
+            pickle.dump((self._data, expiration_time), f)
+        finally:
+            f.close()
     
-    def acquire_lock(self):
-        if not cherrypy.request._session.locked:
-            file_path = self._get_file_path(cherrypy.session.id)
-            self._lock_file(file_path + self.LOCK_SUFFIX)
-            cherrypy.request._session.locked = True
-    
-    def release_lock(self):
-        file_path = self._get_file_path(cherrypy.session.id)
-        self._unlock_file(file_path + self.LOCK_SUFFIX)
-        cherrypy.request._session.locked = False
-    
-    def clean_up(self, sess):
-        storage_path = getattr(sess, "storage_path")
-        if storage_path is None:
-            return
-        now = datetime.datetime.now()
-        # Iterate over all files in the dir/ and exclude non session files
-        #   and lock files
-        for fname in os.listdir(storage_path):
-            if (fname.startswith(self.SESSION_PREFIX)
-                and not fname.endswith(self.LOCK_SUFFIX)):
-                # We have a session file: try to load it and check
-                #   if it's expired. If it fails, nevermind.
-                file_path = os.path.join(storage_path, fname)
-                try:
-                    f = open(file_path, "rb")
-                    data, expiration_time = pickle.load(f)
-                    f.close()
-                    if expiration_time < now:
-                        # Session expired: deleting it
-                        id = fname[len(self.SESSION_PREFIX):]
-                        sess.on_delete(data)
-                        os.unlink(file_path)
-                except:
-                    # We can't access the file ... nevermind
-                    pass
-    
-    def _get_file_path(self, id):
-        storage_path = getattr(cherrypy.request._session, "storage_path")
-        if storage_path is None:
-            raise SessionStoragePathError()
-        fileName = self.SESSION_PREFIX + id
-        file_path = os.path.join(storage_path, fileName)
-        return file_path
-    
-    def _lock_file(self, path):
-        timeout = cherrypy.request._session.deadlock_timeout
-        startTime = time.time()
+    def acquire_lock(self, path=None):
+        if path is None:
+            path = self._get_file_path()
+        path += self.LOCK_SUFFIX
         while True:
             try:
                 lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
             except OSError:
-                if time.time() - startTime > timeout:
-                    raise SessionDeadlockError()
-                time.sleep(0.5)
+                time.sleep(0.1)
             else:
                 os.close(lockfd) 
                 break
+        self.locked = True
     
-    def _unlock_file(self, path):
-        os.unlink(path)
+    def release_lock(self, path=None):
+        if path is None:
+            path = self._get_file_path()
+        os.unlink(path + self.LOCK_SUFFIX)
+        self.locked = False
+    
+    def clean_up(self):
+        """Clean up expired sessions."""
+        now = datetime.datetime.now()
+        # Iterate over all session files in self.storage_path
+        for fname in os.listdir(self.storage_path):
+            if (fname.startswith(self.SESSION_PREFIX)
+                and not fname.endswith(self.LOCK_SUFFIX)):
+                # We have a session file: lock and load it and check
+                #   if it's expired. If it fails, nevermind.
+                path = os.path.join(self.storage_path, fname)
+                self.acquire_lock(path)
+                try:
+                    contents = self._load(path)
+                    # _load returns None on IOError
+                    if contents is not None:
+                        data, expiration_time = contents
+                        if expiration_time < now:
+                            # Session expired: deleting it
+                            os.unlink(path)
+                finally:
+                    self.release_lock(path)
 
 
-class PostgreSQLStorage:
+class PostgresqlSession(Session):
     """ Implementation of the PostgreSQL backend for sessions. It assumes
         a table like this:
 
                 data text,
                 expiration_time timestamp
             )
+    
+    You must provide your own get_db function.
     """
     
     def __init__(self):
-        self.db = cherrypy.request._session.get_db()
+        self.db = self.get_db()
         self.cursor = self.db.cursor()
     
     def __del__(self):
             self.cursor.close()
         self.db.commit()
     
-    def load(self, id):
+    def _load(self):
         # Select session data from table
-        self.cursor.execute(
-            'select data, expiration_time from session where id=%s',
-            (id,))
+        self.cursor.execute('select data, expiration_time from session '
+                            'where id=%s', (self.id,))
         rows = self.cursor.fetchall()
         if not rows:
             return None
+        
         pickled_data, expiration_time = rows[0]
-        # Unpickle data
         data = pickle.loads(pickled_data)
-        return (data, expiration_time)
+        return data, expiration_time
     
-    def save(self, id, data, expiration_time):
-        # Try to delete session if it was already there
-        self.cursor.execute(
-            'delete from session where id=%s',
-            (id,))
-        # Pickle data
-        pickled_data = pickle.dumps(data)
-        # Insert new session data
+    def _save(self, expiration_time):
+        self.cursor.execute('delete from session where id=%s', (self.id,))
+        pickled_data = pickle.dumps(self._data)
         self.cursor.execute(
             'insert into session (id, data, expiration_time) values (%s, %s, %s)',
-            (id, pickled_data, expiration_time))
+            (self.id, pickled_data, expiration_time))
     
     def acquire_lock(self):
         # We use the "for update" clause to lock the row
-        self.cursor.execute(
-            'select id from session where id=%s for update',
-            (cherrypy.session.id,))
+        self.cursor.execute('select id from session where id=%s for update',
+                            (self.id,))
     
     def release_lock(self):
         # We just close the cursor and that will remove the lock
         #   introduced by the "for update" clause
         self.cursor.close()
-        self.cursor = None
     
-    def clean_up(self, sess):
-        now = datetime.datetime.now()
-        self.cursor.execute(
-            'select data from session where expiration_time < %s',
-            (now,))
-        rows = self.cursor.fetchall()
-        for row in rows:
-            sess.on_delete(row[0])
-        self.cursor.execute(
-            'delete from session where expiration_time < %s',
-            (now,))
+    def clean_up(self):
+        """Clean up expired sessions."""
+        self.cursor.execute('delete from session where expiration_time < %s',
+                            (datetime.datetime.now(),))
 
 
-# Users access sessions through cherrypy.session, but we want this
-#   to be thread-specific so we use a wrapper that forwards calls
-#   to cherrypy.session to a thread-specific dictionary called
-#   cherrypy.request._session.data
-class SessionWrapper:
-    
-    def __getattr__(self, name):
-        sess = getattr(cherrypy.request, "_session", None)
-        if sess is None:
-            raise SessionNotEnabledError()
-        
-        # Create thread-specific dictionary if needed
-        if name == 'acquire_lock':
-            return sess.storage.acquire_lock
-        elif name == 'release_lock':
-            return sess.storage.release_lock
-        elif name == 'id':
-            return sess.id
-        
-        if not sess.loaded:
-            data = sess.storage.load(sess.id)
-            # data is either None or a tuple (session_data, expiration_time)
-            if data is None or data[1] < datetime.datetime.now():
-                # Expired session:
-                # flush session data (but keep the same id)
-                sess.data = {'_id': sess.id}
-                if not (data is None):
-                    sess.on_renew(sess.data)
-            else:
-                sess.data = data[0]
-            sess.loaded = True
-
-        return getattr(sess.data, name)
-
-
-# The actual hook functions
+# Hook functions (for CherryPy tools)
 
 def save():
+    """Save any changed session data."""
     def wrap_body(body):
-        # If the body is a generator, we have to save the data
-        #   *after* the generator has been consumed
+        """Response.body wrapper which saves session data."""
         if isinstance(body, types.GeneratorType):
+            # If the body is a generator, we have to save the data
+            #   *after* the generator has been consumed
             for line in body:
                 yield line
-        
-        # Save session data
-        cherrypy.request._session.save()
-        
-        # If the body is not a generator, we save the data
-        #   before the body is returned
-        if not isinstance(body, types.GeneratorType):
+            cherrypy.session.save()
+        else:
+            # If the body is not a generator, we save the data
+            #   before the body is returned (so we can release the lock).
+            cherrypy.session.save()
             for line in body:
                 yield line
     cherrypy.response.body = wrap_body(cherrypy.response.body)
 
-def cleanup():
-    sess = cherrypy.request._session
+def close():
+    """Close the session object for this request."""
+    sess = cherrypy.session
     if sess.locked:
         # If the session is still locked we release the lock
-        sess.storage.release_lock()
-    if sess.storage:
-        sess.storage = None
+        sess.release_lock()
 
+def init(storage_type='ram', path=None, path_header=None, name='session_id',
+         timeout=60, domain=None, secure=False, locking='implicit',
+         clean_freq=5, **kwargs):
+    """Initialize session object (using cookies).
+    
+    Any additional kwargs will be bound to the new Session instance.
+    """
+    
+    request = cherrypy.request
+    
+    # Check if request came with a session ID
+    id = None
+    if name in request.simple_cookie:
+        id = request.simple_cookie[name].value
+    
+    if not hasattr(cherrypy, "session"):
+        cherrypy.session = cherrypy._ThreadLocalProxy('session')
+    
+    # Create and attach a new Session instance to cherrypy.request.
+    # It will possess a reference to (and lock, and lazily load)
+    # the requested session data.
+    storage_class = storage_type.title() + 'Session'
+    cherrypy.serving.session = sess = globals()[storage_class](id)
+    sess.timeout = timeout
+    sess.clean_freq = clean_freq
+    for k, v in kwargs.iteritems():
+        setattr(sess, k, v)
+    
+    if locking == 'implicit':
+        sess.acquire_lock()
+    
+    # Set response cookie
+    cookie = cherrypy.response.simple_cookie
+    cookie[name] = sess.id
+    cookie[name]['path'] = path or request.headers.get(path_header) or '/'
+    
+    # We'd like to use the "max-age" param as indicated in
+    # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
+    # save it to disk and the session is lost if people close
+    # the browser. So we have to use the old "expires" ... sigh ...
+##    cookie[name]['max-age'] = timeout * 60
+    if timeout:
+        cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60))
+    if domain is not None:
+        cookie[name]['domain'] = domain
+    if secure:
+        cookie[name]['secure'] = 1
+

test/test_session.py

 import test
 test.prefer_parent_path()
 
-import cherrypy, os
+import os
+localDir = os.path.dirname(__file__)
+import sys
+import threading
+import time
+
+import cherrypy
 
 
 def setup_server():
     class Root:
         
         _cp_config = {'tools.sessions.on': True,
-                      'tools.sessions.storage_type' : 'file',
-                      'tools.sessions.storage_path' : '.',
+                      'tools.sessions.storage_type' : 'ram',
+                      'tools.sessions.storage_path' : localDir,
+                      'tools.sessions.timeout': 0.017,    # 1.02 secs
+                      'tools.sessions.clean_freq': 0.017,
                       }
         
         def testGen(self):
         testStr.exposed = True
         
         def setsessiontype(self, newtype):
-            cherrypy.config.update({'tools.sessions.storage_type': newtype})
+            self.__class__._cp_config.update({'tools.sessions.storage_type': newtype})
         setsessiontype.exposed = True
         
+        def index(self):
+            sess = cherrypy.session
+            c = sess.get('counter', 0) + 1
+            time.sleep(0.01)
+            sess['counter'] = c
+            return str(c)
+        index.exposed = True
+    
     cherrypy.tree.mount(Root())
     cherrypy.config.update({
             'log_to_screen': False,
 
 class SessionTest(helper.CPWebCase):
     
-    def testSession(self):
+    def test_0_Session(self):
         self.getPage('/testStr')
         self.assertBody('1')
         self.getPage('/testGen', self.cookies)
         self.assertBody('2')
         self.getPage('/testStr', self.cookies)
         self.assertBody('3')
+        
+        # Wait for the session.timeout (1.02 secs)
+        time.sleep(1.25)
+        self.getPage('/')
+        self.assertBody('1')
+        
+        # Wait for the cleanup thread to delete session files
+        f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')]
+        self.assertNotEqual(f(), [])
+        time.sleep(2)
+        self.assertEqual(f(), [])
+    
+    def test_1_Ram_Concurrency(self):
+        self.getPage('/setsessiontype/ram')
+        self._test_Concurrency()
+    
+    def test_2_File_Concurrency(self):
+        self.getPage('/setsessiontype/file')
+        self._test_Concurrency()
+    
+    def _test_Concurrency(self):
+        client_thread_count = 5
+        request_count = 30
+        
+        # Get initial cookie
+        self.getPage("/")
+        self.assertBody("1")
+        cookies = self.cookies
+        
+        data_dict = {}
+        
+        def request(index):
+            for i in xrange(request_count):
+                self.getPage("/", cookies)
+                # Uncomment the following line to prove threads overlap.
+##                print index,
+            data_dict[index] = v = int(self.body)
+        
+        # Start <request_count> concurrent requests from
+        # each of <client_thread_count> clients
+        ts = []
+        for c in xrange(client_thread_count):
+            data_dict[c] = 0
+            t = threading.Thread(target=request, args=(c,))
+            ts.append(t)
+            t.start()
+        
+        for t in ts:
+            t.join()
+        
+        hitcount = max(data_dict.values())
+        expected = 1 + (client_thread_count * request_count)
+        self.assertEqual(hitcount, expected)
 
-        # Clean up session files
-        for fname in os.listdir('.'):
-            if fname.startswith('session-'):
-                os.unlink(fname)
+
 
 if __name__ == "__main__":
     setup_server()
     helper.testmain()
-

test/test_session_concurrency.py

-"""
-Script to simulate lots of concurrent requests to a site that uses sessions.
-It then checks that the integrity of the session data has been kept
-
-
-"""
-
-import cherrypy
-import httplib
-import sys
-import thread
-import time
-
-if len(sys.argv) == 1:
-    print """Usage: test_session_concurrency [storage_type]
-            [storage_path (for file sessions)]
-            [# of server threads] [# of client threads] [# of requests]"""
-    print "Example 1: test_session_concurrency ram"
-    print "Example 2: test_session_concurrency file /tmp"
-    sys.exit(0)
-
-storage_type = sys.argv[1]
-if storage_type == 'file':
-    storage_path = sys.argv[2]
-    i = 3
-else:
-    i = 2
-    storage_path = 'dummy'
-
-try:
-    server_thread_count = sys.argv[i]
-except IndexError:
-    server_thread_count = 10
-try:
-    client_thread_count = sys.argv[i+1]
-except IndexError:
-    client_thread_count = 5
-try:
-    request_count = sys.argv[i+2]
-except IndexError:
-    request_count = 30
-
-# Server code
-class Root:
-    
-    _cp_config = {'session_filter.on': True,
-                  'session_filter.storage_type': storage_type,
-                  'session_filter.storage_path': storage_path,
-                  }
-    
-    def index(self):
-        # If you remove the "acquire_lock" call the assert at the end
-        #   of this script will fail
-        cherrypy.session.acquire_lock()
-        c = cherrypy.session.get('counter', 0) + 1
-        time.sleep(0.1)
-        cherrypy.session['counter'] = c
-        return str(c)
-    index.exposed = True
-
-cherrypy.config.update({
-    'environment': 'production',
-    'log_to_screen': False,
-    'server.thread_pool': server_thread_count,
-})
-cherrypy.tree.mount(Root())
-
-# Client code
-def run_client(cookie, request_count, data_dict, index):
-
-    # Make other requests
-    for i in xrange(request_count):
-        conn = httplib.HTTPConnection('localhost:8080')
-        conn.request("GET", "/", headers = {'Cookie': cookie})
-        r = conn.getresponse()
-        cookie = r.getheader('set-cookie').split(';')[0]
-        data = r.read()
-        conn.close()
-    data_dict[index] = int(data)
-
-# Start server
-cherrypy.server.quickstart()
-thread.start_new_thread(cherrypy.engine.start, ())
-
-# Start client
-time.sleep(2)
-
-# Make first request to get cookie
-conn = httplib.HTTPConnection('localhost:8080')
-conn.request("GET", "/")
-r = conn.getresponse()
-cookie = r.getheader('set-cookie').split(';')[0]
-data = r.read()
-conn.close()
-
-data_dict = {}
-# Simulate <request_count> concurrent requests from <client_thread_count>
-# from the same client
-for i in xrange(client_thread_count):
-    data_dict[i] = 0
-    thread.start_new_thread(run_client, (cookie, request_count, data_dict, i))
-print "Please wait while test is running (default settings take about 30secs)"
-while True:
-    all_finished = True
-    for data in data_dict.values():
-        if data == 0:
-            all_finished = False
-            break
-    if all_finished:
-        break
-    time.sleep(1)
-
-cherrypy.server.stop()
-cherrypy.engine.stop()
-
-m = max(data_dict.values())
-expected_m = 1 + (client_thread_count * request_count)
-if m != expected_m:
-    print "Problem, max is %s instead of %s (data_dict: %s)" % (
-        m, expected_m, data_dict)
-else:
-    print "Everything OK"
-
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.