[Zodb-checkins] SVN: ZODB/trunk/src/ZODB/ Refactored file locking support:

Jim Fulton jim at zope.com
Fri May 18 14:01:55 EDT 2007


Log message for revision 75836:
  Refactored file locking support:
  
  - Found and added a test for a race condition arising from creating
    and removing lock files.
  
  - Change to leave lock files behind.
  
  - Use the windows locking implementation from msvcrt from the standard
    library, rather than using a custom extension module.
  

Changed:
  U   ZODB/trunk/src/ZODB/lock_file.py
  A   ZODB/trunk/src/ZODB/lock_file.txt
  A   ZODB/trunk/src/ZODB/tests/test_lock_file.py

-=-
Modified: ZODB/trunk/src/ZODB/lock_file.py
===================================================================
--- ZODB/trunk/src/ZODB/lock_file.py	2007-05-18 16:54:10 UTC (rev 75835)
+++ ZODB/trunk/src/ZODB/lock_file.py	2007-05-18 18:01:54 UTC (rev 75836)
@@ -17,31 +17,49 @@
 import logging
 logger = logging.getLogger("ZODB.lock_file")
 
+class LockError(Exception):
+    """Couldn't lock a file
+    """
+
 try:
     import fcntl
 except ImportError:
     try:
-        from winlock import LockFile as _LockFile
-        from winlock import UnlockFile as _UnlockFile
+        import msvcrt
     except ImportError:
-        def lock_file(file):
-            logger.info('No file-locking support on this platform')
+        def _lock_file(file):
+            raise TypeError('No file-locking support on this platform')
+        def _unlock_file(file):
+            raise TypeError('No file-locking support on this platform')
 
-    # Windows
-    def lock_file(file):
-        # Lock just the first byte
-        _LockFile(file.fileno(), 0, 0, 1, 0)
+    else:
+        # Windows
+        def _lock_file(file):
+            # Lock just the first byte
+            try:
+                msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
+            except IOError:
+                raise LockError("Couldn't lock %r" % file.name)
 
-    def unlock_file(file):
-        _UnlockFile(file.fileno(), 0, 0, 1, 0)
+        def _unlock_file(file):
+            try:
+                file.seek(0)
+                msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
+            except IOError:
+                raise LockError("Couldn't unlock %r" % file.name)
+                
 else:
     # Unix
     _flags = fcntl.LOCK_EX | fcntl.LOCK_NB
 
-    def lock_file(file):
-        fcntl.flock(file.fileno(), _flags)
+    def _lock_file(file):
+        try:
+            fcntl.flock(file.fileno(), _flags)
+        except IOError:
+            raise LockError("Couldn't lock %r" % file.name)
+            
 
-    def unlock_file(file):
+    def _unlock_file(file):
         # File is automatically unlocked on close
         pass
 
@@ -51,25 +69,29 @@
 # Creating the instance acquires the lock.  The file remains open.  Calling
 # close both closes and unlocks the lock file.
 class LockFile:
+
+    _fp = None
+
     def __init__(self, path):
         self._path = path
+        fp = open(path, 'w+')
+
         try:
-            self._fp = open(path, 'r+')
-        except IOError, e:
-            if e.errno <> errno.ENOENT: raise
-            self._fp = open(path, 'w+')
-        # Acquire the lock and piss on the hydrant
-        try:
-            lock_file(self._fp)
+            _lock_file(fp)
         except:
-            logger.exception("Error locking file %s", path)
+            fp.seek(1)
+            pid = fp.read().strip()[:20]
+            fp.close()
+            logger.exception("Error locking file", path, pid)
             raise
-        print >> self._fp, os.getpid()
-        self._fp.flush()
 
+        self._fp = fp
+        fp.write(" %s\n" % os.getpid())
+        fp.truncate()
+        fp.flush()
+
     def close(self):
         if self._fp is not None:
-            unlock_file(self._fp)
+            _unlock_file(self._fp)
             self._fp.close()
-            os.unlink(self._path)
             self._fp = None

Copied: ZODB/trunk/src/ZODB/lock_file.txt (from rev 75835, ZODB/branches/jim-zeo-blob/src/ZODB/lock_file.txt)
===================================================================
--- ZODB/trunk/src/ZODB/lock_file.txt	                        (rev 0)
+++ ZODB/trunk/src/ZODB/lock_file.txt	2007-05-18 18:01:54 UTC (rev 75836)
@@ -0,0 +1,33 @@
+Lock file support
+=================
+
+The ZODB lock_file module provides support for creating file system
+locks.  These are locks that are implemented with lock files and
+OS-provided locking facilities.  To create a lock, instantiate a
+LockFile object with a file name:
+
+    >>> import ZODB.lock_file
+    >>> lock = ZODB.lock_file.LockFile('lock')
+
+If we try to lock the same name, we'll get a lock error:
+
+    >>> try:
+    ...     ZODB.lock_file.LockFile('lock')
+    ... except ZODB.lock_file.LockError:
+    ...     print "Can't lock file"
+    Can't lock file
+
+To release the lock, use it's close method:
+
+    >>> lock.close()
+
+The lock file is not removed.  It is left behind:
+
+    >>> import os
+    >>> os.path.exists('lock')
+    True
+
+Of course, now that we've released the lock, we can created it again:
+
+    >>> lock = ZODB.lock_file.LockFile('lock')
+    >>> lock.close()

Copied: ZODB/trunk/src/ZODB/tests/test_lock_file.py (from rev 75835, ZODB/branches/jim-zeo-blob/src/ZODB/tests/test_lock_file.py)
===================================================================
--- ZODB/trunk/src/ZODB/tests/test_lock_file.py	                        (rev 0)
+++ ZODB/trunk/src/ZODB/tests/test_lock_file.py	2007-05-18 18:01:54 UTC (rev 75836)
@@ -0,0 +1,58 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import os, sys, unittest
+from zope.testing import doctest
+
+import ZODB.lock_file, time, threading
+    
+
+def inc():
+    while 1:
+        try:
+            lock = ZODB.lock_file.LockFile('f.lock')
+        except ZODB.lock_file.LockError:
+            continue
+        else:
+            break
+    f = open('f', 'r+b')
+    v = int(f.readline().strip())
+    time.sleep(0.01)
+    v += 1
+    f.seek(0)
+    f.write('%d\n' % v)
+    f.close()
+    lock.close()
+
+def many_threads_read_and_write():
+    r"""
+    >>> open('f', 'w+b').write('0\n')
+    >>> open('f.lock', 'w+b').write('0\n')
+
+    >>> n = 50
+    >>> threads = [threading.Thread(target=inc) for i in range(n)]
+    >>> _ = [thread.start() for thread in threads]
+    >>> _ = [thread.join() for thread in threads]
+    >>> saved = int(open('f', 'rb').readline().strip())
+    >>> saved == n
+    True
+
+    >>> os.remove('f')
+    >>> os.remove('f.lock')
+    """
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocFileSuite(os.path.join('..', 'lock_file.txt')))
+    suite.addTest(doctest.DocTestSuite())
+    return suite



More information about the Zodb-checkins mailing list