Commits

Gustavo Picon committed b188923

Merge more robust platform-independent locking technique. Fixes #1122.

Ported from CP3 6b4a141ca59cf298dc40b35ad0a2832c7083b51c

Comments (0)

Files changed (2)

cherrypy/lib/locking.py

+import datetime
+
+
+class NeverExpires(object):
+    def expired(self):
+        return False
+
+
+class Timer(object):
+    """
+    A simple timer that will indicate when an expiration time has passed.
+    """
+    def __init__(self, expiration):
+        "Create a timer that expires at `expiration` (UTC datetime)"
+        self.expiration = expiration
+
+    @classmethod
+    def after(cls, elapsed):
+        """
+        Return a timer that will expire after `elapsed` passes.
+        """
+        return cls(datetime.datetime.utcnow() + elapsed)
+
+    def expired(self):
+        return datetime.datetime.utcnow() >= self.expiration
+
+
+class LockTimeout(Exception):
+    "An exception when a lock could not be acquired before a timeout period"
+
+
+class LockChecker(object):
+    """
+    Keep track of the time and detect if a timeout has expired
+    """
+    def __init__(self, session_id, timeout):
+        self.session_id = session_id
+        if timeout:
+            self.timer = Timer.after(timeout)
+        else:
+            self.timer = NeverExpires()
+
+    def expired(self):
+        if self.timer.expired():
+            raise LockTimeout(
+                "Timeout acquiring lock for %(session_id)s" % vars(self))
+        return False

cherrypy/lib/tools/sessions/file.py

+import datetime
 import os
 import sys
 import time
 import cherrypy
-from cherrypy.lib import lockfile
+from cherrypy.lib import lockfile, locking
 from cherrypy.lib.tools.sessions.base import Session
 from cherrypy.lib.compat import pickle
 
         will be saved as pickle.dump(data, expiration_time) in its own file;
         the filename will be self.SESSION_PREFIX + self.id.
 
+    lock_timeout
+        A timedelta or numeric seconds indicating how long
+        to block acquiring a lock. If None (default), acquiring a lock
+        will block indefinitely.
     """
 
     SESSION_PREFIX = 'session-'
     def __init__(self, id=None, **kwargs):
         # The 'storage_path' arg is required for file-based sessions.
         kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+        kwargs.setdefault('lock_timeout', None)
+
         Session.__init__(self, id=id, **kwargs)
 
+        # validate self.lock_timeout
+        if isinstance(self.lock_timeout, (int, float)):
+            self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
+        if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
+            raise ValueError("Lock timeout must be numeric seconds or "
+                             "a timedelta instance.")
+
     @classmethod
     def setup(cls, **kwargs):
         """Set up the storage system for file-based sessions.
         if path is None:
             path = self._get_file_path()
         path += self.LOCK_SUFFIX
-        while True:
+        checker = locking.LockChecker(self.id, self.lock_timeout)
+        while not checker.expired():
             try:
                 self.lock = lockfile.LockFile(path)
             except lockfile.LockError: