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

Changeset 1232

Show
Ignore:
Timestamp:
08/10/06 12:32:39
Author:
fumanchu
Message:

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.

Files:

Legend:

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

    r1231 r1232  
    7070        return d 
    7171    __dict__ = property(_get_dict) 
     72     
     73    def __getitem__(self, key): 
     74        childobject = getattr(serving, self.__attrname__) 
     75        return childobject[key] 
     76     
     77    def __setitem__(self, key, value): 
     78        childobject = getattr(serving, self.__attrname__) 
     79        childobject[key] = value 
    7280 
    7381 
  • trunk/cherrypy/_cptools.py

    r1222 r1232  
    160160    """Session Tool for CherryPy.""" 
    161161     
    162     def __init__(self): 
    163         self._point = "before_finalize" 
    164         self.callable = _sessions.save 
    165         self._name = None 
    166         for k in dir(_sessions.Session): 
    167             if k not in ("init", "save") and not k.startswith("__"): 
    168                 setattr(self, k, None) 
    169      
    170     def _init(self): 
    171         conf = cherrypy.request.toolmap.get(self._name, {}) 
    172          
    173         s = cherrypy.request._session = _sessions.Session() 
    174         # Copy all conf entries onto Session object attributes 
    175         for k, v in conf.iteritems(): 
    176             setattr(s, str(k), v) 
    177         s.init() 
    178          
    179         if not hasattr(cherrypy, "session"): 
    180             cherrypy.session = _sessions.SessionWrapper() 
    181      
    182162    def _setup(self): 
    183163        """Hook this tool into cherrypy.request using the given conf. 
     
    186166        method when the tool is "turned on" in config. 
    187167        """ 
    188         # init must be bound after headers are read 
    189         cherrypy.request.hooks.attach('before_request_body', self._init) 
     168        Tool._setup(self) 
    190169        cherrypy.request.hooks.attach('before_finalize', _sessions.save) 
    191         cherrypy.request.hooks.attach('on_end_request', _sessions.cleanup
     170        cherrypy.request.hooks.attach('on_end_request', _sessions.close
    192171 
    193172 
     
    303282default_toolbox.staticdir = MainTool(static.staticdir) 
    304283default_toolbox.staticfile = MainTool(static.staticfile) 
    305 default_toolbox.sessions = SessionTool() 
     284# _sessions.init must be bound after headers are read 
     285default_toolbox.sessions = SessionTool('before_request_body', _sessions.init) 
    306286default_toolbox.xmlrpc = XMLRPCTool() 
    307287default_toolbox.wsgiapp = WSGIAppTool(_wsgiapp.run) 
  • trunk/cherrypy/lib/cptools.py

    r1225 r1232  
    126126    request = cherrypy.request 
    127127    tdata = cherrypy.thread_data 
    128     sess = getattr(cherrypy, "session", None) 
    129     if sess is None: 
    130         # Shouldn't this raise an error (if the sessions tool isn't enabled)? 
    131         return False 
    132      
     128    sess = cherrypy.session 
    133129    request.user = None 
    134130    tdata.user = None 
    135131     
    136 ##    conf = cherrypy.config.get 
    137 ##    if conf('tools.staticfile.on', False) or conf('tools.staticdir.on', False): 
    138 ##        return 
    139132    if request.path.endswith('login_screen'): 
    140133        return False 
  • trunk/cherrypy/lib/sessions.py

    r1179 r1232  
    44well as data about the session for the current request. Instead of 
    55polluting cherrypy.request we use a Session object bound to 
    6 cherrypy.request._session to store these variables. 
    7  
    8 Global variables (RAM backend only): 
    9     - _session_lock_dict: dictionary containing the locks for all session_id 
    10     - _session_data_holder: dictionary containing the data for all sessions 
    11  
     6cherrypy.session to store these variables. 
    127""" 
    138 
     
    2116import sha 
    2217import time 
    23 import thread 
    2418import threading 
    2519import types 
     
    2822from cherrypy.lib import http 
    2923 
    30 _session_last_clean_up_time = datetime.datetime.now() 
    31 _session_data_holder = {} # Needed for RAM sessions only 
    32 _session_lock_dict = {} # Needed for RAM sessions only 
    33  
    34  
    35 def generate_id(self=None): 
    36     """Return a new session id""" 
    37     return sha.new('%s' % random.random()).hexdigest() 
    38  
    39  
    40 def noop(self, data=None): pass 
    41  
    42 class Session: 
    43     """A CherryPy Session object (one per request). 
    44      
    45     timeout: timeout delay for the session 
    46     locking: mechanism used to lock the session ('implicit' or 'explicit') 
    47     storage (instance of the class implementing the backend) 
    48     data: dictionary containing the actual session data 
    49     id: current session ID 
    50     expiration_time: date/time when the current session will expire 
     24 
     25class Session(object): 
     26    """A CherryPy dict-like Session object (one per request). 
     27     
     28    id: current session ID. 
     29    expiration_time (datetime): when the current session will expire. 
     30    timeout (minutes): used to calculate expiration_time from now. 
     31    clean_freq (minutes): the poll rate for expired session cleanup. 
     32    locked: If True, this session instance has exclusive read/write access 
     33        to session data. 
     34    loaded: If True, data has been retrieved from storage. This should 
     35        happen automatically on the first attempt to access session data. 
    5136    """ 
    5237     
    53     # It's important that the following are class variables, 
    54     # so that the CherryPy SessionTool can grab them and fake tooltips. 
    55     timeout = 60 
    56     locking = 'explicit' 
    57     deadlock_timeout = 30 
    58     clean_up_delay = 5 
    59     storage_type = 'Ram' 
    60     storage_class = None 
    61     cookie_name = 'session_id' 
    62     cookie_domain = None 
    63     cookie_secure = False 
    64     cookie_path = None 
    65     cookie_path_from_header = None 
    66     generate_id = generate_id 
    67     on_create = noop 
    68     on_renew = noop 
    69     on_delete = noop 
    70      
    71     def __init__(self): 
    72         self.storage = None 
     38    clean_thread = None 
     39     
     40    def __init__(self, id=None): 
    7341        self.locked = False 
    7442        self.loaded = False 
    75         self.saved = False 
     43        self._data = {} 
    7644         
    77         self.generate_id = generate_id 
    78         self.on_create = noop 
    79         self.on_renew = noop 
    80         self.on_delete = noop 
    81      
    82     def init(self): 
    83         # People can set their own custom class 
    84         #   through tools.sessions.storage_class 
    85         if self.storage_class is None: 
    86             self.storage_class = globals()[self.storage_type.title() + 'Storage'] 
    87         self.storage = self.storage_class() 
    88          
    89         now = datetime.datetime.now() 
    90         # Check if we need to clean up old sessions 
    91         global _session_last_clean_up_time 
    92         clean_up_delay = datetime.timedelta(seconds = self.clean_up_delay * 60) 
    93         if _session_last_clean_up_time + clean_up_delay < now: 
    94             _session_last_clean_up_time = now 
    95             # Run clean_up in other thread to avoid blocking this request 
    96             thread.start_new_thread(self.storage.clean_up, (self,)) 
    97          
    98         self.data = {} 
    99          
    100         if self.cookie_path is None: 
    101             if self.cookie_path_from_header is not None: 
    102                 geth = cherrypy.request.headers.get 
    103                 self.cookie_path = geth(self.cookie_path_from_header, None) 
    104             if self.cookie_path is None: 
    105                 self.cookie_path = '/' 
    106          
    107         # Check if request came with a session ID 
    108         if self.cookie_name in cherrypy.request.simple_cookie: 
    109             # It did: we mark the data as needing to be loaded 
    110             self.id = cherrypy.request.simple_cookie[self.cookie_name].value 
    111              
    112             # If using implicit locking, acquire lock 
    113             if self.locking == 'implicit': 
    114                 self.data['_id'] = self.id 
    115                 self.storage.acquire_lock() 
    116         else: 
    117             # No id yet 
    118             self.id = self.generate_id() 
    119             self.data['_id'] =  self.id 
    120             self.on_create(self.data) 
    121          
    122         # Set response cookie 
    123         cookie = cherrypy.response.simple_cookie 
    124         cookie[self.cookie_name] = self.id 
    125         cookie[self.cookie_name]['path'] = self.cookie_path 
    126         # We'd like to use the "max-age" param as 
    127         #   http://www.faqs.org/rfcs/rfc2109.html indicates but IE doesn't 
    128         #   save it to disk and the session is lost if people close 
    129         #   the browser 
    130         #   So we have to use the old "expires" ... sigh ... 
    131         #cookie[cookie_name]['max-age'] = self.timeout * 60 
    132         if self.timeout: 
    133             expiry = time.time() + (self.timeout * 60) 
    134             cookie[self.cookie_name]['expires'] = http.HTTPDate(expiry) 
    135         if self.cookie_domain is not None: 
    136             cookie[self.cookie_name]['domain'] = self.cookie_domain 
    137         if self.cookie_secure is True: 
    138             cookie[self.cookie_name]['secure'] = 1 
     45        if id is None: 
     46            id = self.generate_id() 
     47        self.id = id 
     48     
     49    def clean_cycle(self): 
     50        """Clean up expired sessions at regular intervals.""" 
     51        # clean_thread is both a cancelable Timer and a flag. 
     52        if self.clean_thread: 
     53            self.clean_up() 
     54            t = threading.Timer(self.clean_freq, self.clean_cycle) 
     55            self.__class__.clean_thread = t 
     56            t.start() 
     57     
     58    def clean_interrupt(cls): 
     59        """Stop the expired-session cleaning cycle.""" 
     60        if cls.clean_thread: 
     61            cls.clean_thread.cancel() 
     62            cls.clean_thread = None 
     63    clean_interrupt = classmethod(clean_interrupt) 
     64     
     65    def clean_up(self): 
     66        """Clean up expired sessions.""" 
     67        pass 
     68     
     69    def generate_id(self): 
     70        """Return a new session id.""" 
     71        return sha.new('%s' % random.random()).hexdigest() 
    13972     
    14073    def save(self): 
    141         """Save session data""" 
     74        """Save session data.""" 
    14275        # If session data has never been loaded then it's never been 
    14376        #   accessed: no need to delete it 
     
    14578            t = datetime.timedelta(seconds = self.timeout * 60) 
    14679            expiration_time = datetime.datetime.now() + t 
    147             self.storage.save(self.id, self.data, expiration_time) 
     80            self._save(expiration_time) 
    14881         
    14982        if self.locked: 
    15083            # Always release the lock if the user didn't release it 
    151             self.storage.release_lock() 
     84            self.release_lock() 
     85     
     86    def load(self): 
     87        """Copy stored session data into this session instance.""" 
     88        data = self._load() 
     89        # data is either None or a tuple (session_data, expiration_time) 
     90        if data is None or data[1] < datetime.datetime.now(): 
     91            # Expired session: flush session data (but keep the same id) 
     92            self._data = {} 
     93        else: 
     94            self._data = data[0] 
     95        self.loaded = True 
    15296         
    153         self.saved = True 
    154  
    155  
    156 class SessionDeadlockError(Exception): 
    157     """The session could not acquire a lock after a certain time""" 
    158     pass 
    159  
    160  
    161 class SessionNotEnabledError(Exception): 
    162     """User forgot to set tools.sessions.on to True""" 
    163     pass 
    164  
    165 class SessionStoragePathError(Exception): 
    166     """User set storage_type to file but forgot to set the storage_path""" 
    167     pass 
    168  
    169  
    170 class RamStorage: 
    171     """ Implementation of the RAM backend for sessions """ 
    172      
    173     def load(self, id): 
    174         return _session_data_holder.get(id) 
    175      
    176     def save(self, id, data, expiration_time): 
    177         _session_data_holder[id] = (data, expiration_time) 
     97        cls = self.__class__ 
     98        if not cls.clean_thread: 
     99            cherrypy.engine.on_stop_engine_list.append(cls.clean_interrupt) 
     100            # Use the instance to call clean_cycle so tool config 
     101            # can be accessed inside the method. 
     102            cls.clean_thread = t = threading.Timer(self.clean_freq, 
     103                                                   self.clean_cycle) 
     104            t.start() 
     105     
     106    def __getitem__(self, key): 
     107        if not self.loaded: self.load() 
     108        return self._data[key] 
     109     
     110    def __setitem__(self, key, value): 
     111        if not self.loaded: self.load() 
     112        self._data[key] = value 
     113     
     114    def __delitem__(self, key): 
     115        if not self.loaded: self.load() 
     116        del self._data[key] 
     117     
     118    def __contains__(self, key): 
     119        if not self.loaded: self.load() 
     120        return key in self._data 
     121     
     122    def has_key(self, key): 
     123        if not self.loaded: self.load() 
     124        return self._data.has_key(key) 
     125     
     126    def get(self, key, default=None): 
     127        if not self.loaded: self.load() 
     128        return self._data.get(key, default) 
     129     
     130    def update(self, d): 
     131        if not self.loaded: self.load() 
     132        self._data.update(d) 
     133     
     134    def setdefault(self, key, default=None): 
     135        if not self.loaded: self.load() 
     136        return self._data.setdefault(key, default) 
     137     
     138    def clear(self): 
     139        if not self.loaded: self.load() 
     140        self._data.clear() 
     141     
     142    def keys(self): 
     143        if not self.loaded: self.load() 
     144        return self._data.keys() 
     145     
     146    def items(self): 
     147        if not self.loaded: self.load() 
     148        return self._data.items() 
     149     
     150    def values(self): 
     151        if not self.loaded: self.load() 
     152        return self._data.values() 
     153 
     154 
     155class RamSession(Session): 
     156     
     157    # Class-level objects. Don't rebind these! 
     158    cache = {} 
     159    locks = {} 
     160     
     161    def clean_up(self): 
     162        """Clean up expired sessions.""" 
     163        now = datetime.datetime.now() 
     164        for id, (data, expiration_time) in self.cache.items(): 
     165            if expiration_time < now: 
     166                try: 
     167                    del self.cache[id] 
     168                except KeyError: 
     169                    pass 
     170     
     171    def _load(self): 
     172        return self.cache.get(self.id) 
     173     
     174    def _save(self, expiration_time): 
     175        self.cache[self.id] = (self._data, expiration_time) 
    178176     
    179177    def acquire_lock(self): 
    180         sess = cherrypy.request._session 
    181         id = cherrypy.session.id 
    182         lock = _session_lock_dict.get(id) 
    183         if lock is None: 
    184             lock = threading.Lock() 
    185             _session_lock_dict[id] = lock 
    186         startTime = time.time() 
    187         while True: 
    188             if lock.acquire(False): 
    189                 break 
    190             if time.time() - startTime > sess.deadlock_timeout: 
    191                 raise SessionDeadlockError() 
    192             time.sleep(0.5) 
    193         sess.locked = True 
     178        self.locked = True 
     179        self.locks.setdefault(self.id, threading.Semaphore()).acquire() 
    194180     
    195181    def release_lock(self): 
    196         _session_lock_dict[cherrypy.session['_id']].release() 
    197         cherrypy.request._session.locked = False 
    198      
    199     def clean_up(self, sess): 
    200         to_be_deleted = [] 
    201         now = datetime.datetime.now() 
    202         for id, (data, expiration_time) in _session_data_holder.iteritems(): 
    203             if expiration_time < now: 
    204                 to_be_deleted.append(id) 
    205         for id in to_be_deleted: 
    206             try: 
    207                 deleted_session = _session_data_holder[id] 
    208                 del _session_data_holder[id] 
    209                 sess.on_delete(deleted_session) 
    210             except KeyError: 
    211                 # The session probably got deleted by a concurrent thread 
    212                 #   Safe to ignore this case 
    213                 pass 
    214  
    215  
    216 class FileStorage: 
     182        self.locks[self.id].release() 
     183        self.locked = False 
     184 
     185 
     186class FileSession(Session): 
    217187    """ Implementation of the File backend for sessions """ 
    218188     
     
    220190    LOCK_SUFFIX = '.lock' 
    221191     
    222     def load(self, id): 
    223         file_path = self._get_file_path(id) 
     192    def _get_file_path(self): 
     193        return os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) 
     194     
     195    def _load(self, path=None): 
     196        if path is None: 
     197            path = self._get_file_path() 
    224198        try: 
    225             f = open(file_path, "rb") 
    226             data = pickle.load(f) 
    227             f.close() 
    228             return data 
     199            f = open(path, "rb") 
     200            try: 
     201                return pickle.load(f) 
     202            finally: 
     203                f.close() 
    229204        except (IOError, EOFError): 
    230205            return None 
    231206     
    232     def save(self, id, data, expiration_time): 
    233         file_path = self._get_file_path(id) 
    234         f = open(file_path, "wb") 
    235         pickle.dump((data, expiration_time), f) 
    236         f.close() 
    237      
    238     def acquire_lock(self): 
    239         if not cherrypy.request._session.locked: 
    240             file_path = self._get_file_path(cherrypy.session.id) 
    241             self._lock_file(file_path + self.LOCK_SUFFIX) 
    242             cherrypy.request._session.locked = True 
    243      
    244     def release_lock(self): 
    245         file_path = self._get_file_path(cherrypy.session.id) 
    246         self._unlock_file(file_path + self.LOCK_SUFFIX) 
    247         cherrypy.request._session.locked = False 
    248      
    249     def clean_up(self, sess): 
    250         storage_path = getattr(sess, "storage_path") 
    251         if storage_path is None: 
    252             return 
    253         now = datetime.datetime.now() 
    254         # Iterate over all files in the dir/ and exclude non session files 
    255         #   and lock files 
    256         for fname in os.listdir(storage_path): 
    257             if (fname.startswith(self.SESSION_PREFIX) 
    258                 and not fname.endswith(self.LOCK_SUFFIX)): 
    259                 # We have a session file: try to load it and check 
    260                 #   if it's expired. If it fails, nevermind. 
    261                 file_path = os.path.join(storage_path, fname) 
    262                 try: 
    263                     f = open(file_path, "rb") 
    264                     data, expiration_time = pickle.load(f) 
    265                     f.close() 
    266                     if expiration_time < now: 
    267                         # Session expired: deleting it 
    268                         id = fname[len(self.SESSION_PREFIX):] 
    269                         sess.on_delete(data) 
    270                         os.unlink(file_path) 
    271                 except: 
    272                     # We can't access the file ... nevermind 
    273                     pass 
    274      
    275     def _get_file_path(self, id): 
    276         storage_path = getattr(cherrypy.request._session, "storage_path") 
    277         if storage_path is None: 
    278             raise SessionStoragePathError() 
    279         fileName = self.SESSION_PREFIX + id 
    280         file_path = os.path.join(storage_path, fileName) 
    281         return file_path 
    282      
    283     def _lock_file(self, path): 
    284         timeout = cherrypy.request._session.deadlock_timeout 
    285         startTime = time.time() 
     207    def _save(self, expiration_time): 
     208        f = open(self._get_file_path(), "wb") 
     209        try: 
     210            pickle.dump((self._data, expiration_time), f) 
     211        finally: 
     212            f.close() 
     213     
     214    def acquire_lock(self, path=None): 
     215        if path is None: 
     216            path = self._get_file_path() 
     217        path += self.LOCK_SUFFIX 
    286218        while True: 
    287219            try: 
    288220                lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) 
    289221            except OSError: 
    290                 if time.time() - startTime > timeout: 
    291                     raise SessionDeadlockError() 
    292                 time.sleep(0.5) 
     222                time.sleep(0.1) 
    293223            else: 
    294224                os.close(lockfd)  
    295225                break 
    296      
    297     def _unlock_file(self, path): 
    298         os.unlink(path) 
    299  
    300  
    301 class PostgreSQLStorage: 
     226        self.locked = True 
     227     
     228    def release_lock(self, path=None): 
     229        if path is None: 
     230            path = self._get_file_path() 
     231        os.unlink(path + self.LOCK_SUFFIX) 
     232        self.locked = False 
     233     
     234    def clean_up(self): 
     235        """Clean up expired sessions.""" 
     236        now = datetime.datetime.now() 
     237        # Iterate over all session files in self.storage_path 
     238        for fname in os.listdir(self.storage_path): 
     239            if (fname.startswith(self.SESSION_PREFIX) 
     240                and not fname.endswith(self.LOCK_SUFFIX)): 
     241                # We have a session file: lock and load it and check 
     242                #   if it's expired. If it fails, nevermind. 
     243                path = os.path.join(self.storage_path, fname) 
     244                self.acquire_lock(path) 
     245                try: 
     246                    contents = self._load(path) 
     247                    # _load returns None on IOError 
     248                    if contents is not None: 
     249                        data, expiration_time = contents 
     250                        if expiration_time < now: 
     251                            # Session expired: deleting it 
     252                            os.unlink(path) 
     253                finally: 
     254                    self.release_lock(path) 
     255 
     256 
     257class PostgresqlSession(Session): 
    302258    """ Implementation of the PostgreSQL backend for sessions. It assumes 
    303259        a table like this: 
     
    308264                expiration_time timestamp 
    309265            ) 
     266     
     267    You must provide your own get_db function. 
    310268    """ 
    311269     
    312270    def __init__(self): 
    313         self.db = cherrypy.request._session.get_db() 
     271        self.db = self.get_db() 
    314272        self.cursor = self.db.cursor() 
    315273     
     
    319277        self.db.commit() 
    320278     
    321     def load(self, id): 
     279    def _load(self): 
    322280        # Select session data from table 
    323         self.cursor.execute( 
    324             'select data, expiration_time from session where id=%s', 
    325             (id,)) 
     281        self.cursor.execute('select data, expiration_time from session ' 
     282                            'where id=%s', (self.id,)) 
    326283        rows = self.cursor.fetchall() 
    327284        if not rows: 
    328285            return None 
     286         
    329287        pickled_data, expiration_time = rows[0] 
    330         # Unpickle data 
    331288        data = pickle.loads(pickled_data) 
    332         return (data, expiration_time) 
    333      
    334     def save(self, id, data, expiration_time): 
    335         # Try to delete session if it was already there 
    336         self.cursor.execute( 
    337             'delete from session where id=%s', 
    338             (id,)) 
    339         # Pickle data 
    340         pickled_data = pickle.dumps(data) 
    341         # Insert new session data 
     289        return data, expiration_time 
     290     
     291    def _save(self, expiration_time): 
     292        self.cursor.execute('delete from session where id=%s', (self.id,)) 
     293        pickled_data = pickle.dumps(self._data) 
    342294        self.cursor.execute( 
    343295            'insert into session (id, data, expiration_time) values (%s, %s, %s)', 
    344             (id, pickled_data, expiration_time)) 
     296            (self.id, pickled_data, expiration_time)) 
    345297     
    346298    def acquire_lock(self): 
    347299        # We use the "for update" clause to lock the row 
    348         self.cursor.execute( 
    349             'select id from session where id=%s for update', 
    350             (cherrypy.session.id,)) 
     300        self.cursor.execute('select id from session where id=%s for update', 
     301                            (self.id,)) 
    351302     
    352303    def release_lock(self): 
     
    354305        #   introduced by the "for update" clause 
    355306        self.cursor.close() 
    356         self.cursor = None 
    357      
    358     def clean_up(self, sess): 
    359         now = datetime.datetime.now() 
    360         self.cursor.execute( 
    361             'select data from session where expiration_time < %s', 
    362             (now,)) 
    363         rows = self.cursor.fetchall() 
    364         for row in rows: 
    365             sess.on_delete(row[0]) 
    366         self.cursor.execute( 
    367             'delete from session where expiration_time < %s', 
    368             (now,)) 
    369  
    370  
    371 # Users access sessions through cherrypy.session, but we want this 
    372 #   to be thread-specific so we use a wrapper that forwards calls 
    373 #   to cherrypy.session to a thread-specific dictionary called 
    374 #   cherrypy.request._session.data 
    375 class SessionWrapper: 
    376      
    377     def __getattr__(self, name): 
    378         sess = getattr(cherrypy.request, "_session", None) 
    379         if sess is None: 
    380             raise SessionNotEnabledError() 
    381          
    382         # Create thread-specific dictionary if needed 
    383         if name == 'acquire_lock': 
    384             return sess.storage.acquire_lock 
    385         elif name == 'release_lock': 
    386             return sess.storage.release_lock 
    387         elif name == 'id': 
    388             return sess.id 
    389          
    390         if not sess.loaded: 
    391             data = sess.storage.load(sess.id) 
    392             # data is either None or a tuple (session_data, expiration_time) 
    393             if data is None or data[1] < datetime.datetime.now(): 
    394                 # Expired session: 
    395                 # flush session data (but keep the same id) 
    396                 sess.data = {'_id': sess.id} 
    397                 if not (data is None): 
    398                     sess.on_renew(sess.data) 
    399             else: 
    400                 sess.data = data[0] 
    401             sess.loaded = True 
    402  
    403         return getattr(sess.data, name) 
    404  
    405  
    406 # The actual hook functions 
     307     
     308    def clean_up(self): 
     309        """Clean up expired sessions.""" 
     310        self.cursor.execute('delete from session where expiration_time < %s', 
     311                            (datetime.datetime.now(),)) 
     312 
     313 
     314# Hook functions (for CherryPy tools) 
    407315 
    408316def save(): 
     317    """Save any changed session data.""" 
    409318    def wrap_body(body): 
    410         # If the body is a generator, we have to save the data 
    411         #   *after* the generator has been consumed 
     319        """Response.body wrapper which saves session data.""" 
    412320        if isinstance(body, types.GeneratorType): 
     321            # If the body is a generator, we have to save the data 
     322            #   *after* the generator has been consumed 
    413323            for line in body: 
    414324                yield line 
    415          
    416         # Save session data 
    417         cherrypy.request._session.save() 
    418          
    419         # If the body is not a generator, we save the data 
    420         #   before the body is returned 
    421         if not isinstance(body, types.GeneratorType): 
     325            cherrypy.session.save() 
     326        else: 
     327            # If the body is not a generator, we save the data 
     328            #   before the body is returned (so we can release the lock). 
     329            cherrypy.session.save() 
    422330            for line in body: 
    423331                yield line 
    424332    cherrypy.response.body = wrap_body(cherrypy.response.body) 
    425333 
    426 def cleanup(): 
    427     sess = cherrypy.request._session 
     334def close(): 
     335    """Close the session object for this request.""" 
     336    sess = cherrypy.session 
    428337    if sess.locked: 
    429338        # If the session is still locked we release the lock 
    430         sess.storage.release_lock() 
    431     if sess.storage: 
    432         sess.storage = None 
    433  
     339        sess.release_lock() 
     340 
     341def init(storage_type='ram', path=None, path_header=None, name='session_id', 
     342         timeout=60, domain=None, secure=False, locking='implicit', 
     343         clean_freq=5, **kwargs): 
     344    """Initialize session object (using cookies). 
     345     
     346    Any additional kwargs will be bound to the new Session instance. 
     347    """ 
     348     
     349    request = cherrypy.request 
     350     
     351    # Check if request came with a session ID 
     352    id = None 
     353    if name in request.simple_cookie: 
     354        id = request.simple_cookie[name].value 
     355     
     356    if not hasattr(cherrypy, "session"): 
     357        cherrypy.session = cherrypy._ThreadLocalProxy('session') 
     358     
     359    # Create and attach a new Session instance to cherrypy.request. 
     360    # It will possess a reference to (and lock, and lazily load) 
     361    # the requested session data. 
     362    storage_class = storage_type.title() + 'Session' 
     363    cherrypy.serving.session = sess = globals()[storage_class](id) 
     364    sess.timeout = timeout 
     365    sess.clean_freq = clean_freq 
     366    for k, v in kwargs.iteritems(): 
     367        setattr(sess, k, v) 
     368     
     369    if locking == 'implicit': 
     370        sess.acquire_lock() 
     371     
     372    # Set response cookie 
     373    cookie = cherrypy.response.simple_cookie 
     374    cookie[name] = sess.id 
     375    cookie[name]['path'] = path or request.headers.get(path_header) or '/' 
     376     
     377    # We'd like to use the "max-age" param as indicated in 
     378    # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't 
     379    # save it to disk and the session is lost if people close 
     380    # the browser. So we have to use the old "expires" ... sigh ... 
     381##    cookie[name]['max-age'] = timeout * 60 
     382    if timeout: 
     383        cookie[name]['expires'] = http.HTTPDate(time.time() + (timeout * 60)) 
     384    if domain is not None: 
     385        cookie[name]['domain'] = domain 
     386    if secure: 
     387        cookie[name]['secure'] = 1 
     388 
  • trunk/cherrypy/test/test_session.py

    r1114 r1232  
    22test.prefer_parent_path() 
    33 
    4 import cherrypy, os 
     4import os 
     5localDir = os.path.dirname(__file__) 
     6import sys 
     7import threading 
     8import time 
     9 
     10import cherrypy 
    511 
    612 
     
    915         
    1016        _cp_config = {'tools.sessions.on': True, 
    11                       'tools.sessions.storage_type' : 'file', 
    12                       'tools.sessions.storage_path' : '.', 
     17                      'tools.sessions.storage_type' : 'ram', 
     18                      'tools.sessions.storage_path' : localDir, 
     19                      'tools.sessions.timeout': 0.017,    # 1.02 secs 
     20                      'tools.sessions.clean_freq': 0.017, 
    1321                      } 
    1422         
     
    2634         
    2735        def setsessiontype(self, newtype): 
    28             cherrypy.config.update({'tools.sessions.storage_type': newtype}) 
     36            self.__class__._cp_config.update({'tools.sessions.storage_type': newtype}) 
    2937        setsessiontype.exposed = True 
    3038         
     39        def index(self): 
     40            sess = cherrypy.session 
     41            c = sess.get('counter', 0) + 1 
     42            time.sleep(0.01) 
     43            sess['counter'] = c 
     44            return str(c) 
     45        index.exposed = True 
     46     
    3147    cherrypy.tree.mount(Root()) 
    3248    cherrypy.config.update({ 
     
    3955class SessionTest(helper.CPWebCase): 
    4056     
    41     def testSession(self): 
     57    def test_0_Session(self): 
    4258        self.getPage('/testStr') 
    4359        self.assertBody('1') 
     
    5369        self.getPage('/testStr', self.cookies) 
    5470        self.assertBody('3') 
     71         
     72        # Wait for the session.timeout (1.02 secs) 
     73        time.sleep(1.25) 
     74        self.getPage('/') 
     75        self.assertBody('1') 
     76         
     77        # Wait for the cleanup thread to delete session files 
     78        f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] 
     79        self.assertNotEqual(f(), []) 
     80        time.sleep(2) 
     81        self.assertEqual(f(), []) 
     82     
     83    def test_1_Ram_Concurrency(self): 
     84        self.getPage('/setsessiontype/ram') 
     85        self._test_Concurrency() 
     86     
     87    def test_2_File_Concurrency(self): 
     88        self.getPage('/setsessiontype/file') 
     89        self._test_Concurrency() 
     90     
     91    def _test_Concurrency(self): 
     92        client_thread_count = 5 
     93        request_count = 30 
     94         
     95        # Get initial cookie 
     96        self.getPage("/") 
     97        self.assertBody("1") 
     98        cookies = self.cookies 
     99         
     100        data_dict = {} 
     101         
     102        def request(index): 
     103            for i in xrange(request_count): 
     104                self.getPage("/", cookies) 
     105                # Uncomment the following line to prove threads overlap. 
     106##                print index, 
     107            data_dict[index] = v = int(self.body) 
     108         
     109        # Start <request_count> concurrent requests from 
     110        # each of <client_thread_count> clients 
     111        ts = [] 
     112        for c in xrange(client_thread_count): 
     113            data_dict[c] = 0 
     114            t = threading.Thread(target=request, args=(c,)) 
     115            ts.append(t) 
     116            t.start() 
     117         
     118        for t in ts: 
     119            t.join() 
     120         
     121        hitcount = max(data_dict.values()) 
     122        expected = 1 + (client_thread_count * request_count) 
     123        self.assertEqual(hitcount, expected) 
    55124 
    56         # Clean up session files 
    57         for fname in os.listdir('.'): 
    58             if fname.startswith('session-'): 
    59                 os.unlink(fname) 
     125 
    60126 
    61127if __name__ == "__main__": 
    62128    setup_server() 
    63129    helper.testmain() 
    64  

Hosted by WebFaction

Log in as guest/cpguest to create tickets