Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 597

Show
Ignore:
Timestamp:
09/03/05 05:43:19
Author:
rdelon
Message:

More session improvements: support cleaning up old sessions, support notifying users when a session is created/deleted, new PostgreSQL backend

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/__init__.py

    r581 r597  
    3333__version__ = '2.1.0-beta' 
    3434 
     35import datetime 
     36 
    3537from _cperror import * 
    36  
    3738import config 
    3839import server 
     
    6061threadData = local() 
    6162 
     63# Create variables needed for session (see lib/sessionfilter.py for more info) 
    6264from lib.filter import sessionfilter 
    6365session = sessionfilter.SessionWrapper() 
     66_sessionDataHolder = {} # Needed for RAM sessions only 
     67_sessionLockDict = {} # Needed for RAM sessions only 
     68_sessionLastCleanUpTime = datetime.datetime.now() 
    6469 
    6570# decorator function for exposing methods 
  • trunk/cherrypy/lib/filter/sessionfilter.py

    r582 r597  
    2828 
    2929""" Session implementation for CherryPy. 
    30 We use cherrypy.threadData (td) to store some convenient variables as 
    31 well as data about the session for the current request. 
     30We use cherrypy.threadData to store some convenient variables as 
     31well as data about the session for the current request. Instead of 
     32polluting cherrypy.threadData we use a dummy object called 
     33cherrypy.threadData._session (sess) to store these variables. 
    3234 
    3335Variables used to store config options: 
    34     - td._sessionTimeout: timeout delay for the session 
    35     - td._sessionLocking: mechanism used to lock the session ('implicit' or 'explicit') 
     36    - sess.sessionTimeout: timeout delay for the session 
     37    - sess.sessionLocking: mechanism used to lock the session ('implicit' or 'explicit') 
    3638 
    3739Variables used to store temporary variables: 
    38     - td._sessionStorage (instance of the class implementing the backend) 
     40    - sess.sessionStorage (instance of the class implementing the backend) 
    3941 
    4042 
    4143Variables used to store the session for the current request: 
    42     - td._sessionData: dictionnary containing the actual session data 
    43     - td._sessionID: current session ID 
    44     - td._expirationTime: time when the current session will expire 
     44    - sess.sessionData: dictionary containing the actual session data 
     45    - sess.sessionID: current session ID 
     46    - sess.expirationTime: date/time when the current session will expire 
    4547 
    4648Global variables (RAM backend only): 
    47     - cherrypy._sessionLockDict: dictionnary containing the locks for all sessionIDs 
    48     - cherrypy._sessionHolder: dictionnary containing the data for all sessions 
     49    - cherrypy._sessionLockDict: dictionary containing the locks for all sessionIDs 
     50    - cherrypy._sessionHolder: dictionary containing the data for all sessions 
    4951 
    5052""" 
    5153 
     54import datetime 
    5255import sha 
    5356import os 
    5457import pickle 
    5558import random 
     59import StringIO 
     60import time 
    5661import threading 
    57 import time 
     62import types 
    5863 
    5964import basefilter 
    6065 
    61 # TODO: Clean up old sessions 
    62 # TODO: Release stale locks after a certain time 
     66class EmptyClass: 
     67    """ An empty class """ 
     68    pass 
     69 
     70class SessionDeadlockError(Exception): 
     71    """ Happens when a session can't acquire a lock after a 
     72        certain time 
     73    """ 
     74    pass 
    6375 
    6476class SessionFilter(basefilter.BaseFilter): 
     
    6678        # We have to dynamically import cherrypy because Python can't handle 
    6779        #   circular module imports :-( 
    68         global cherrypy, td 
     80        global cherrypy 
    6981        import cherrypy 
    70         td = cherrypy.threadData 
    71         if not cherrypy.config.get('sessionFilter.on', False): 
    72             td._sessionStorage = None 
     82        cherrypy.threadData._session = EmptyClass() 
     83        sess = cherrypy.threadData._session 
     84        now = datetime.datetime.now() 
     85        # Dont enable session if sessionFilter is off or if this is a 
     86        #   request for static data 
     87        if (not cherrypy.config.get('sessionFilter.on', False)) or \ 
     88                cherrypy.config.get('staticFilter.on', False): 
     89            sess.sessionStorage = None 
    7390            return 
    7491 
     92        sess.locked = False # Not locked by default 
     93 
    7594        # Read config options 
    76         td._sessionTimeout = \ 
     95        sess.sessionTimeout = \ 
    7796            cherrypy.config.get('sessionFilter.timeout', 60) 
    7897 
    79         td._sessionLocking = \ 
     98        sess.sessionLocking = \ 
    8099            cherrypy.config.get('sessionFilter.locking', 'implicit') 
     100 
     101        sess.onCreateSession = \ 
     102            cherrypy.config.get('sessionFilter.onCreateSession', 
     103                lambda data: None) 
     104 
     105        sess.onDeleteSession = \ 
     106            cherrypy.config.get('sessionFilter.onDeleteSession', 
     107                lambda data: None) 
     108 
     109        cleanUpDelay = \ 
     110            cherrypy.config.get('sessionFilter.cleanUpDelay', 5) 
    81111 
    82112        cookieName = \ 
    83113            cherrypy.config.get('sessionFilter.cookieName', 'sessionID') 
    84114 
     115        sess.deadlockTimeout = \ 
     116            cherrypy.config.get('sessionFilter.deadlockTimeout', 30) 
     117 
    85118        storage = cherrypy.config.get('sessionFilter.storageType', 'Ram') 
    86         storage = storage.capitalize() 
    87         # TODO: support custom storage types 
    88         td._sessionStorage = globals()[storage + 'Storage']() 
     119        storage = storage[0].upper() + storage[1:] 
     120        # TODO: support custom storage types (allow users to pass 
     121        #   their own class through another config option) 
     122        sess.sessionStorage = globals()[storage + 'Storage']() 
     123 
     124        # Check if we need to clean up old sessions 
     125        if cherrypy._sessionLastCleanUpTime + \ 
     126                datetime.timedelta(seconds = cleanUpDelay * 60) < now: 
     127            sess.sessionStorage.cleanUp() 
    89128 
    90129        # Check if request came with a session ID 
    91130        if cookieName in cherrypy.request.simpleCookie: 
    92131            # It did: we try to load the session data 
    93             td._sessionID = cherrypy.request.simpleCookie[cookieName].value 
    94             data = td._sessionStorage.load(td._sessionID) 
     132            sess.sessionID = cherrypy.request.simpleCookie[cookieName].value 
     133            # If using implicit locking, acquire lock 
     134            if sess.sessionLocking == 'implicit': 
     135                sess.sessionData = {'_id': sess.sessionID} 
     136                sess.sessionStorage.acquireLock() 
     137            data = sess.sessionStorage.load(sess.sessionID) 
    95138            # data is either None or a tuple (sessionData, expirationTime) 
    96             if data is None or data[1] < time.time()
     139            if data is None or data[1] < now
    97140                # Expired session: flush session data (but keep the same 
    98141                #   sessionID) 
    99                 td._sessionData = {
     142                sess.sessionData = {'_id': sess.sessionID
    100143            else: 
    101                 td._sessionData = data[0] 
     144                sess.sessionData = data[0] 
    102145        else: 
    103146            # No sessionID yet 
    104             td._sessionID = generateSessionID() 
    105             td._sessionData = {
    106         td._sessionData['_id'] = td._sessionID 
     147            sess.sessionID = generateSessionID() 
     148            sess.sessionData = {'_id': sess.sessionID
     149            sess.onCreateSession(sess.sessionData) 
    107150        # Set response cookie 
    108         cherrypy.response.simpleCookie[cookieName] = td._sessionID 
     151        cherrypy.response.simpleCookie[cookieName] = sess.sessionID 
    109152        cherrypy.response.simpleCookie[cookieName]['path'] = '/' 
    110153        cherrypy.response.simpleCookie[cookieName]['max-age'] = \ 
    111             td._sessionTimeout * 60 
     154            sess.sessionTimeout * 60 
    112155        cherrypy.response.simpleCookie[cookieName]['version'] = 1 
    113156 
    114         # If using implicit locking, acquire lock 
    115         if td._sessionLocking == 'implicit': 
    116             td._sessionStorage.acquireLock() 
    117  
    118157    def beforeFinalize(self): 
    119         if not td._sessionStorage: 
     158        def returnBodyAndSaveData(body, sess): 
     159            # If the body is a generator, we have to save the data 
     160            #   *after* the generator has been consumed 
     161            if isinstance(body, types.GeneratorType): 
     162                for line in body: 
     163                    yield line 
     164 
     165            # Save session data 
     166            expirationTime = datetime.datetime.now() + \ 
     167                    datetime.timedelta(seconds = sess.sessionTimeout * 60) 
     168            sess.sessionStorage.save( 
     169                    sess.sessionID, sess.sessionData, expirationTime) 
     170            if sess.locked: 
     171                # Always release the lock if the user didn't release it 
     172                sess.sessionStorage.releaseLock() 
     173 
     174            # If the body is not a generator, we save the data 
     175            #   before the body is returned 
     176            if not isinstance(body, types.GeneratorType): 
     177                for line in body: 
     178                    yield line 
     179 
     180        sess = cherrypy.threadData._session 
     181        if not sess.sessionStorage: 
     182            # Sessions are not enabled: do nothing 
    120183            return 
    121         # Save session data 
    122         expirationTime = time.time() + td._sessionTimeout * 60 
    123         td._sessionStorage.save( 
    124                 td._sessionID, (td._sessionData, expirationTime)) 
    125         try: 
    126             # Always try to release the lock at the end 
    127             td._sessionStorage.releaseLock() 
    128         except: 
    129             pass 
     184 
     185        # Make a wrapper around the body in order to save the session 
     186        #   either before or after the body is returned 
     187        cherrypy.response.body = \ 
     188            returnBodyAndSaveData(cherrypy.response.body, sess) 
     189 
    130190 
    131191    def onEndResource(self): 
    132         try: 
    133             # Try to release the lock one more time at the very end (in 
    134             #   case there was an error while processing the request 
    135             #   or something) 
    136             td._sessionStorage.releaseLock() 
    137         except: 
    138             pass 
     192        sess = cherrypy.threadData._session 
     193        if getattr(sess, 'locked', None): 
     194            # If the session is still locked there probably was an 
     195            #   error while processing the request. 
     196            #   In that case we release the lock anyway. 
     197            sess.sessionStorage.releaseLock() 
     198        if getattr(sess, 'sessionStorage', None): 
     199            del sess.sessionStorage 
    139200 
    140201class RamStorage: 
    141202    """ Implementation of the RAM backend for sessions """ 
    142     def __init__(self): 
    143         try: 
    144             cherrypy._sessionDataHolder 
    145         except: 
    146             cherrypy._sessionDataHolder = {} 
    147         try: 
    148             cherrypy._sessionLockDict 
    149         except: 
    150             cherrypy._sessionLockDict = {} 
    151  
    152203    def load(self, id): 
    153204        return cherrypy._sessionDataHolder.get(id) 
    154     def save(self, id, data): 
    155         cherrypy._sessionDataHolder[id] = data 
     205    def save(self, id, data, expirationTime): 
     206        cherrypy._sessionDataHolder[id] = (data, expirationTime) 
    156207    def acquireLock(self): 
     208        sess = cherrypy.threadData._session 
    157209        id = cherrypy.session['_id'] 
    158210        lock = cherrypy._sessionLockDict.get(id) 
     
    160212            lock = threading.Lock() 
    161213            cherrypy._sessionLockDict[id] = lock 
    162         lock.acquire() 
     214        startTime = time.time() 
     215        while True: 
     216            if lock.acquire(False): 
     217                break 
     218            if time.time() - startTime > sess.deadlockTimeout: 
     219                raise SessionDeadlockError() 
     220        sess.locked = True 
    163221    def releaseLock(self): 
     222        sess = cherrypy.threadData._session 
    164223        id = cherrypy.session['_id'] 
    165224        cherrypy._sessionLockDict[id].release() 
     225        sess.locked = False 
     226    def cleanUp(self): 
     227        sess = cherrypy.threadData._session 
     228        toBeDeleted = [] 
     229        now = datetime.datetime.now() 
     230        for id, (data, expirationTime) in cherrypy._sessionDataHolder.iteritems(): 
     231            if expirationTime < now: 
     232                toBeDeleted.append(id) 
     233        for id in toBeDeleted: 
     234            sess.onDeleteSession(cherrypy._sessionDataHolder[id]) 
     235            del cherrypy._sessionDataHolder[id] 
    166236 
    167237class FileStorage: 
    168238    """ Implementation of the File backend for sessions """ 
     239    SESSION_PREFIX = 'session-' 
     240    LOCK_SUFFIX = '.lock' 
    169241    def load(self, id): 
    170242        filePath = self._getFilePath(id) 
     
    174246            f.close() 
    175247            return data 
    176         except
    177             return (None, None) 
    178     def save(self, id, data): 
     248        except IOError
     249            return None 
     250    def save(self, id, data, expirationTime): 
    179251        filePath = self._getFilePath(id) 
    180252        f = open(filePath, "wb") 
    181         pickle.dump(data, f) 
     253        pickle.dump((data, expirationTime), f) 
    182254        f.close() 
    183255    def acquireLock(self): 
    184         # Use the OS to acquire a lock on the file. 
    185         # This means that if we have multiple CP processes it'll still 
    186         # work fine 
     256        sess = cherrypy.threadData._session 
    187257        filePath = self._getFilePath(cherrypy.session['_id']) 
    188         lockFilePath = filePath + '.lock' 
     258        lockFilePath = filePath + self.LOCK_SUFFIX 
     259        self._lockFile(lockFilePath) 
     260        sess.locked = True 
     261 
     262    def releaseLock(self): 
     263        sess = cherrypy.threadData._session 
     264        filePath = self._getFilePath(cherrypy.session['_id']) 
     265        lockFilePath = filePath + self.LOCK_SUFFIX 
     266        self._unlockFile(lockFilePath) 
     267        sess.locked = False 
     268 
     269    def cleanUp(self): 
     270        sess = cherrypy.threadData._session 
     271        storagePath = cherrypy.config.get('sessionFilter.storagePath') 
     272        now = datetime.datetime.now() 
     273        # Iterate over all files in the dir/ and exclude non session files 
     274        #   and lock files 
     275        for fname in os.listdir(storagePath): 
     276            if fname.startswith(self.SESSION_PREFIX) and \ 
     277                        (not fname.endswith(self.LOCK_SUFFIX)): 
     278                # We have a session file: lock it, load it and check 
     279                #   if it's expired 
     280                filePath = os.path.join(storagePath, fname) 
     281                lockFilePath = filePath + self.LOCK_SUFFIX 
     282                self._lockFile(lockFilePath) 
     283                try: 
     284                    f = open(filePath, "rb") 
     285                    (data, expirationTime) = pickle.load(f) 
     286                    f.close() 
     287                    if expirationTime < now: 
     288                        # Session expired: deleting it 
     289                        id = fname[len(self.SESSION_PREFIX):] 
     290                        sess.onDeleteSession(data) 
     291                        os.unlink(filePath) 
     292                except IOError: 
     293                    # We can't access the file ... nevermind 
     294                    pass 
     295                self._unlockFile(lockFilePath) 
     296 
     297    def _getFilePath(self, id): 
     298        storagePath = cherrypy.config.get('sessionFilter.storagePath') 
     299        fileName = self.SESSION_PREFIX + id 
     300        filePath = os.path.join(storagePath, fileName) 
     301        return filePath 
     302 
     303    def _lockFile(self, path): 
     304        sess = cherrypy.threadData._session 
     305        startTime = time.time() 
    189306        while True: 
    190307            try: 
    191                 lockfd = os.open(lockFilePath, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 
     308                lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 
    192309            except OSError: 
    193                 pass 
     310                if time.time() - startTime > sess.deadlockTimeout: 
     311                    raise SessionDeadlockError() 
    194312            else: 
    195313                os.close(lockfd)  
    196314                break 
     315    def _unlockFile(self, path): 
     316        os.unlink(path) 
     317 
     318class PostgreSQLStorage: 
     319    """ Implementation of the PostgreSQL backend for sessions. It assumes 
     320        a table like this: 
     321 
     322            create table session ( 
     323                id varchar(40), 
     324                data text, 
     325                expiration_time timestamp 
     326            ) 
     327    """ 
     328    def __init__(self): 
     329        self.db = cherrypy.config.get('sessionFilter.getDB')() 
     330        self.cursor = self.db.cursor() 
     331    def __del__(self): 
     332        if self.cursor: 
     333            self.cursor.close() 
     334        self.db.commit() 
     335    def load(self, id): 
     336        # Select session data from table 
     337        self.cursor.execute( 
     338            'select data, expiration_time from session where id=%s', 
     339            (id,)) 
     340        rows = self.cursor.fetchall() 
     341        if not rows: 
     342            return None 
     343        data, expirationTime = rows[0] 
     344        # Unpickle data 
     345        f = StringIO.StringIO(data) 
     346        data = pickle.load(f) 
     347        return (data, expirationTime) 
     348    def save(self, id, data, expirationTime): 
     349        # Try to delete session if it was already there 
     350        self.cursor.execute( 
     351            'delete from session where id=%s', 
     352            (id,)) 
     353        # Pickle data 
     354        f = StringIO.StringIO() 
     355        pickle.dump(data, f) 
     356        # Insert new session data 
     357        self.cursor.execute( 
     358            'insert into session (id, data, expiration_time) values (%s, %s, %s)', 
     359            (id, f.getvalue(), expirationTime)) 
     360 
     361    def acquireLock(self): 
     362        # We use the "for update" clause to lock the row 
     363        self.cursor.execute( 
     364            'select id from session where id=%s for update', 
     365            (cherrypy.session['_id'],)) 
    197366 
    198367    def releaseLock(self): 
    199         filePath = self._getFilePath(cherrypy.session['_id']) 
    200         lockFilePath = filePath + '.lock' 
    201         os.unlink(lockFilePath) 
    202  
    203     def _getFilePath(self, id): 
    204         storagePath = cherrypy.config.get('sessionFilter.storagePath') 
    205         fileName = 'sessionFile-' + id 
    206         filePath = os.path.join(storagePath, fileName) 
    207         return filePath 
    208  
     368        # We just close the cursor and that will remove the lock 
     369        #   introduced by the "for update" clause 
     370        self.cursor.close() 
     371        self.cursor = None 
     372    def cleanUp(self): 
     373        sess = cherrypy.threadData._session 
     374        now = datetime.datetime.now() 
     375        self.cursor.execute( 
     376            'select data from session where expiration_time < %s', 
     377            (now,)) 
     378        rows = self.cursor.fetchall() 
     379        for row in rows: 
     380            sess.onDeleteSession(row[0]) 
     381        self.cursor.execute( 
     382            'delete from session where expiration_time < %s', 
     383            (now,)) 
    209384 
    210385def generateSessionID(): 
    211         """ Function to return a new sessionID """ 
     386        """ Return a new sessionID """ 
    212387        return sha.new('%s' % random.random()).hexdigest() 
    213388 
    214389# Users access sessions through cherrypy.session, but we want this 
    215390#   to be thread-specific so we use a special wrapper that forwards 
    216 #   calls to cherrypy.session to a thread-specific dictionnary called 
    217 #   cherrypy.threadData._sessionData 
     391#   calls to cherrypy.session to a thread-specific dictionary called 
     392#   cherrypy.threadData._session.sessionData 
    218393class SessionWrapper(object): 
    219394    def __getattribute__(self, name): 
    220         # Create thread-specific dictionnary if needed 
    221         try: 
    222             td._sessionData 
    223         except: 
    224             td._sessionData = {} 
     395        # Create thread-specific dictionary if needed 
     396        sess = cherrypy.threadData._session 
     397        sess.sessionData = getattr(sess, 'sessionData', {}) 
    225398        if name == 'acquireLock': 
    226             return td._sessionStorage.acquireLock 
     399            return sess.sessionStorage.acquireLock 
    227400        elif name == 'releaseLock': 
    228             return td._sessionStorage.releaseLock 
    229         return td._sessionData.__getattribute__(name) 
     401            return sess.sessionStorage.releaseLock 
     402        return sess.sessionData.__getattribute__(name) 
    230403    def __getitem__(self, *a, **b): 
    231         return td._sessionData.__getitem__(*a, **b) 
     404        sess = cherrypy.threadData._session 
     405        return sess.sessionData.__getitem__(*a, **b) 
    232406    def __setitem__(self, *a, **b): 
    233         return td._sessionData.__setitem__(*a, **b) 
     407        sess = cherrypy.threadData._session 
     408        return sess.sessionData.__setitem__(*a, **b) 
     409    def __delitem__(self, *a, **b): 
     410        sess = cherrypy.threadData._session 
     411        return sess.sessionData.__delitem__(*a, **b) 
  • trunk/cherrypy/test/test_session_filter.py

    r581 r597  
    2727""" 
    2828 
    29 import cherrypy 
     29import cherrypy, time, os 
    3030 
    3131class Root: 
    32     def index(self): 
     32    def testGen(self): 
    3333        counter = cherrypy.session.get('counter', 0) + 1 
    3434        cherrypy.session['counter'] = counter 
    3535        yield str(counter) 
    36     index.exposed = True 
     36    testGen.exposed = True 
     37    def testStr(self): 
     38        counter = cherrypy.session.get('counter', 0) + 1 
     39        cherrypy.session['counter'] = counter 
     40        return str(counter) 
     41    testStr.exposed = True 
    3742     
    3843cherrypy.root = Root() 
     
    4146        'server.environment': 'production', 
    4247        'sessionFilter.on': True, 
     48        'sessionFilter.storageType' : 'file', 
     49        'sessionFilter.storagePath' : '.', 
    4350}) 
    4451 
     
    4855     
    4956    def testSessionFilter(self): 
    50         self.getPage('/') 
     57        self.getPage('/testStr') 
    5158        self.assertBody('1') 
    52         self.getPage('/', self.cookies) 
     59        self.getPage('/testGen', self.cookies) 
    5360        self.assertBody('2') 
    54         self.getPage('/', self.cookies) 
     61        self.getPage('/testStr', self.cookies) 
    5562        self.assertBody('3') 
     63        cherrypy.config.update({ 
     64            'sessionFilter.storageType' : 'file', 
     65        }) 
     66        self.getPage('/testStr') 
     67        self.assertBody('1') 
     68        self.getPage('/testGen', self.cookies) 
     69        self.assertBody('2') 
     70        self.getPage('/testStr', self.cookies) 
     71        self.assertBody('3') 
     72 
     73        # Clean up session files 
     74        for fname in os.listdir('.'): 
     75            if fname.startswith('session-'): 
     76                os.unlink(fname) 
     77         
    5678         
    5779if __name__ == "__main__": 

Hosted by WebFaction

Log in as guest/cpguest to create tickets