lockfile.py

Go to the documentation of this file.
00001 
00002 ##
00003 # 
00004 # lockfile.py - Platform-independent advisory file locks.
00005 # 
00006 # Requires Python 2.5 unless you apply 2.4.diff
00007 # Locking is done on a per-thread basis instead of a per-process basis.
00008 # 
00009 # Usage:
00010 # 
00011 # >>> lock = FileLock(_testfile())
00012 # >>> try:
00013 # ...     lock.acquire()
00014 # ... except AlreadyLocked:
00015 # ...     print _testfile(), 'is locked already.'
00016 # ... except LockFailed:
00017 # ...     print _testfile(), 'can\\'t be locked.'
00018 # ... else:
00019 # ...     print 'got lock'
00020 # got lock
00021 # >>> print lock.is_locked()
00022 # True
00023 # >>> lock.release()
00024 # 
00025 # >>> lock = FileLock(_testfile())
00026 # >>> print lock.is_locked()
00027 # False
00028 # >>> with lock:
00029 # ...    print lock.is_locked()
00030 # True
00031 # >>> print lock.is_locked()
00032 # False
00033 # >>> # It is okay to lock twice from the same thread...
00034 # >>> with lock:
00035 # ...     lock.acquire()
00036 # ...
00037 # >>> # Though no counter is kept, so you can't unlock multiple times...
00038 # >>> print lock.is_locked()
00039 # False
00040 # 
00041 # Exceptions:
00042 # 
00043 #     Error - base class for other exceptions
00044 #         LockError - base class for all locking exceptions
00045 #             AlreadyLocked - Another thread or process already holds the lock
00046 #             LockFailed - Lock failed for some other reason
00047 #         UnlockError - base class for all unlocking exceptions
00048 #             AlreadyUnlocked - File was not locked.
00049 #             NotMyLock - File was locked but not by the current thread/process
00050 # 
00051 # To do:
00052 #     * Write more test cases
00053 #       - verify that all lines of code are executed
00054 #     * Describe on-disk file structures in the documentation.
00055 # 
00056 
00057 from __future__ import division, with_statement
00058 
00059 import socket
00060 import os
00061 import threading
00062 import time
00063 import errno
00064 import thread
00065 
00066 ##
00067 # 
00068 #     Base class for other exceptions.
00069 # 
00070 #     >>> try:
00071 #     ...   raise Error
00072 #     ... except Exception:
00073 #     ...   pass
00074 #     
00075 class Error(Exception):
00076     pass
00077 
00078 ##
00079 # 
00080 #     Base class for error arising from attempts to acquire the lock.
00081 # 
00082 #     >>> try:
00083 #     ...   raise LockError
00084 #     ... except Error:
00085 #     ...   pass
00086 #     
00087 class LockError(Error):
00088     pass
00089 
00090 ##
00091 # Raised when lock creation fails within a user-defined period of time.
00092 # 
00093 #     >>> try:
00094 #     ...   raise LockTimeout
00095 #     ... except LockError:
00096 #     ...   pass
00097 #     
00098 class LockTimeout(LockError):
00099     pass
00100 
00101 ##
00102 # Some other thread/process is locking the file.
00103 # 
00104 #     >>> try:
00105 #     ...   raise AlreadyLocked
00106 #     ... except LockError:
00107 #     ...   pass
00108 #     
00109 class AlreadyLocked(LockError):
00110     pass
00111 
00112 ##
00113 # Lock file creation failed for some other reason.
00114 # 
00115 #     >>> try:
00116 #     ...   raise LockFailed
00117 #     ... except LockError:
00118 #     ...   pass
00119 #     
00120 class LockFailed(LockError):
00121     pass
00122 
00123 ##
00124 # 
00125 #     Base class for errors arising from attempts to release the lock.
00126 # 
00127 #     >>> try:
00128 #     ...   raise UnlockError
00129 #     ... except Error:
00130 #     ...   pass
00131 #     
00132 class UnlockError(Error):
00133     pass
00134 
00135 ##
00136 # Raised when an attempt is made to unlock an unlocked file.
00137 # 
00138 #     >>> try:
00139 #     ...   raise NotLocked
00140 #     ... except UnlockError:
00141 #     ...   pass
00142 #     
00143 class NotLocked(UnlockError):
00144     pass
00145 
00146 ##
00147 # Raised when an attempt is made to unlock a file someone else locked.
00148 # 
00149 #     >>> try:
00150 #     ...   raise NotMyLock
00151 #     ... except UnlockError:
00152 #     ...   pass
00153 #     
00154 class NotMyLock(UnlockError):
00155     pass
00156 
00157 ##
00158 # Base class for platform-specific lock classes.
00159 class LockBase:
00160     ##
00161     # 
00162     #         >>> lock = LockBase(_testfile())
00163     #         
00164     def __init__(self, path, threaded=True):
00165         self.path = path
00166         self.lock_file = os.path.abspath(path) + ".lock"
00167         self.hostname = socket.gethostname()
00168         self.pid = os.getpid()
00169         if threaded:
00170             tname = "%x-" % thread.get_ident()
00171         else:
00172             tname = ""
00173         dirname = os.path.dirname(self.lock_file)
00174         self.unique_name = os.path.join(dirname,
00175                                         "%s.%s%s" % (self.hostname,
00176                                                      tname,
00177                                                      self.pid))
00178 
00179     ##
00180     # 
00181     #         Acquire the lock.
00182     # 
00183     #         * If timeout is omitted (or None), wait forever trying to lock the
00184     #           file.
00185     # 
00186     #         * If timeout > 0, try to acquire the lock for that many seconds.  If
00187     #           the lock period expires and the file is still locked, raise
00188     #           LockTimeout.
00189     # 
00190     #         * If timeout <= 0, raise AlreadyLocked immediately if the file is
00191     #           already locked.
00192     # 
00193     #         >>> # As simple as it gets.
00194     #         >>> lock = FileLock(_testfile())
00195     #         >>> lock.acquire()
00196     #         >>> lock.release()
00197     # 
00198     #         >>> # No timeout test
00199     #         >>> e1, e2 = threading.Event(), threading.Event()
00200     #         >>> t = _in_thread(_lock_wait_unlock, e1, e2)
00201     #         >>> e1.wait()         # wait for thread t to acquire lock
00202     #         >>> lock2 = FileLock(_testfile())
00203     #         >>> lock2.is_locked()
00204     #         True
00205     #         >>> lock2.i_am_locking()
00206     #         False
00207     #         >>> try:
00208     #         ...   lock2.acquire(timeout=-1)
00209     #         ... except AlreadyLocked:
00210     #         ...   pass
00211     #         ... except Exception, e:
00212     #         ...   print 'unexpected exception', repr(e)
00213     #         ... else:
00214     #         ...   print 'thread', threading.currentThread().getName(),
00215     #         ...   print 'erroneously locked an already locked file.'
00216     #         ...   lock2.release()
00217     #         ...
00218     #         >>> e2.set()          # tell thread t to release lock
00219     #         >>> t.join()
00220     # 
00221     #         >>> # Timeout test
00222     #         >>> e1, e2 = threading.Event(), threading.Event()
00223     #         >>> t = _in_thread(_lock_wait_unlock, e1, e2)
00224     #         >>> e1.wait() # wait for thread t to acquire filelock
00225     #         >>> lock2 = FileLock(_testfile())
00226     #         >>> lock2.is_locked()
00227     #         True
00228     #         >>> try:
00229     #         ...   lock2.acquire(timeout=0.1)
00230     #         ... except LockTimeout:
00231     #         ...   pass
00232     #         ... except Exception, e:
00233     #         ...   print 'unexpected exception', repr(e)
00234     #         ... else:
00235     #         ...   lock2.release()
00236     #         ...   print 'thread', threading.currentThread().getName(),
00237     #         ...   print 'erroneously locked an already locked file.'
00238     #         ...
00239     #         >>> e2.set()
00240     #         >>> t.join()
00241     #         
00242     def acquire(self, timeout=None):
00243         pass
00244 
00245     ##
00246     # 
00247     #         Release the lock.
00248     # 
00249     #         If the file is not locked, raise NotLocked.
00250     #         >>> lock = FileLock(_testfile())
00251     #         >>> lock.acquire()
00252     #         >>> lock.release()
00253     #         >>> lock.is_locked()
00254     #         False
00255     #         >>> lock.i_am_locking()
00256     #         False
00257     #         >>> try:
00258     #         ...   lock.release()
00259     #         ... except NotLocked:
00260     #         ...   pass
00261     #         ... except NotMyLock:
00262     #         ...   print 'unexpected exception', NotMyLock
00263     #         ... except Exception, e:
00264     #         ...   print 'unexpected exception', repr(e)
00265     #         ... else:
00266     #         ...   print 'erroneously unlocked file'
00267     # 
00268     #         >>> e1, e2 = threading.Event(), threading.Event()
00269     #         >>> t = _in_thread(_lock_wait_unlock, e1, e2)
00270     #         >>> e1.wait()
00271     #         >>> lock2 = FileLock(_testfile())
00272     #         >>> lock2.is_locked()
00273     #         True
00274     #         >>> lock2.i_am_locking()
00275     #         False
00276     #         >>> try:
00277     #         ...   lock2.release()
00278     #         ... except NotMyLock:
00279     #         ...   pass
00280     #         ... except Exception, e:
00281     #         ...   print 'unexpected exception', repr(e)
00282     #         ... else:
00283     #         ...   print 'erroneously unlocked a file locked by another thread.'
00284     #         ...
00285     #         >>> e2.set()
00286     #         >>> t.join()
00287     #         
00288     def release(self):
00289         pass
00290 
00291     ##
00292     # 
00293     #         Tell whether or not the file is locked.
00294     #         >>> lock = FileLock(_testfile())
00295     #         >>> lock.acquire()
00296     #         >>> lock.is_locked()
00297     #         True
00298     #         >>> lock.release()
00299     #         >>> lock.is_locked()
00300     #         False
00301     #         
00302     def is_locked(self):
00303         pass
00304 
00305     ##
00306     # Return True if this object is locking the file.
00307     # 
00308     #         >>> lock1 = FileLock(_testfile(), threaded=False)
00309     #         >>> lock1.acquire()
00310     #         >>> lock2 = FileLock(_testfile())
00311     #         >>> lock1.i_am_locking()
00312     #         True
00313     #         >>> lock2.i_am_locking()
00314     #         False
00315     #   >>> try:
00316     #   ...   lock2.acquire(timeout=2)
00317     #   ... except LockTimeout:
00318     #         ...   lock2.break_lock()
00319     #         ...   lock2.is_locked()
00320     #         ...   lock1.is_locked()
00321     #         ...   lock2.acquire()
00322     #         ... else:
00323     #         ...   print 'expected LockTimeout...'
00324     #         ...
00325     #         False
00326     #         False
00327     #         >>> lock1.i_am_locking()
00328     #         False
00329     #         >>> lock2.i_am_locking()
00330     #         True
00331     #         >>> lock2.release()
00332     #         
00333     def i_am_locking(self):
00334         pass
00335 
00336     ##
00337     # Remove a lock.  Useful if a locking thread failed to unlock.
00338     # 
00339     #         >>> lock = FileLock(_testfile())
00340     #         >>> lock.acquire()
00341     #         >>> lock2 = FileLock(_testfile())
00342     #         >>> lock2.is_locked()
00343     #         True
00344     #         >>> lock2.break_lock()
00345     #         >>> lock2.is_locked()
00346     #         False
00347     #         >>> try:
00348     #         ...   lock.release()
00349     #         ... except NotLocked:
00350     #         ...   pass
00351     #         ... except Exception, e:
00352     #         ...   print 'unexpected exception', repr(e)
00353     #         ... else:
00354     #         ...   print 'break lock failed'
00355     #         
00356     def break_lock(self):
00357         pass
00358 
00359     ##
00360     # Context manager support.
00361     # 
00362     #         >>> lock = FileLock(_testfile())
00363     #         >>> with lock:
00364     #         ...   lock.is_locked()
00365     #         ...
00366     #         True
00367     #         >>> lock.is_locked()
00368     #         False
00369     #         
00370     def __enter__(self):
00371         self.acquire()
00372         return self
00373 
00374     ##
00375     # Context manager support.
00376     # 
00377     #         >>> 'tested in __enter__'
00378     #         'tested in __enter__'
00379     #         
00380     def __exit__(self, *_exc):
00381         self.release()
00382 
00383 ##
00384 # Lock access to a file using atomic property of link(2).
00385 class LinkFileLock(LockBase):
00386 
00387     ##
00388     # 
00389     #         >>> d = _testfile()
00390     #         >>> os.mkdir(d)
00391     #         >>> os.chmod(d, 0444)
00392     #         >>> try:
00393     #         ...   lock = LinkFileLock(os.path.join(d, 'test'))
00394     #         ...   try:
00395     #         ...     lock.acquire()
00396     #         ...   except LockFailed:
00397     #         ...     pass
00398     #         ...   else:
00399     #         ...     lock.release()
00400     #         ...     print 'erroneously locked', os.path.join(d, 'test')
00401     #         ... finally:
00402     #         ...   os.chmod(d, 0664)
00403     #         ...   os.rmdir(d)
00404     #         
00405     def acquire(self, timeout=None):
00406         try:
00407             open(self.unique_name, "wb").close()
00408         except IOError:
00409             raise LockFailed
00410 
00411         end_time = time.time()
00412         if timeout is not None and timeout > 0:
00413             end_time += timeout
00414 
00415         while True:
00416             # Try and create a hard link to it.
00417             try:
00418                 os.link(self.unique_name, self.lock_file)
00419             except OSError:
00420                 # Link creation failed.  Maybe we've double-locked?
00421                 nlinks = os.stat(self.unique_name).st_nlink
00422                 if nlinks == 2:
00423                     # The original link plus the one I created == 2.  We're
00424                     # good to go.
00425                     return
00426                 else:
00427                     # Otherwise the lock creation failed.
00428                     if timeout is not None and time.time() > end_time:
00429                         os.unlink(self.unique_name)
00430                         if timeout > 0:
00431                             raise LockTimeout
00432                         else:
00433                             raise AlreadyLocked
00434                     time.sleep(timeout is not None and timeout/10 or 0.1)
00435             else:
00436                 # Link creation succeeded.  We're good to go.
00437                 return
00438 
00439     def release(self):
00440         if not self.is_locked():
00441             raise NotLocked
00442         elif not os.path.exists(self.unique_name):
00443             raise NotMyLock
00444         os.unlink(self.unique_name)
00445         os.unlink(self.lock_file)
00446 
00447     def is_locked(self):
00448         return os.path.exists(self.lock_file)
00449 
00450     def i_am_locking(self):
00451         return (self.is_locked() and
00452                 os.path.exists(self.unique_name) and
00453                 os.stat(self.unique_name).st_nlink == 2)
00454 
00455     def break_lock(self):
00456         if os.path.exists(self.lock_file):
00457             os.unlink(self.lock_file)
00458 
00459 ##
00460 # Lock file by creating a directory.
00461 class MkdirFileLock(LockBase):
00462     ##
00463     # 
00464     #         >>> lock = MkdirFileLock(_testfile())
00465     #         
00466     def __init__(self, path, threaded=True):
00467         LockBase.__init__(self, path)
00468         if threaded:
00469             tname = "%x-" % thread.get_ident()
00470         else:
00471             tname = ""
00472         # Lock file itself is a directory.  Place the unique file name into
00473         # it.
00474         self.unique_name  = os.path.join(self.lock_file,
00475                                          "%s.%s%s" % (self.hostname,
00476                                                       tname,
00477                                                       self.pid))
00478 
00479     def acquire(self, timeout=None):
00480         end_time = time.time()
00481         if timeout is not None and timeout > 0:
00482             end_time += timeout
00483 
00484         if timeout is None:
00485             wait = 0.1
00486         else:
00487             wait = max(0, timeout / 10)
00488 
00489         while True:
00490             try:
00491                 os.mkdir(self.lock_file)
00492             except OSError, err:
00493                 if err.errno == errno.EEXIST:
00494                     # Already locked.
00495                     if os.path.exists(self.unique_name):
00496                         # Already locked by me.
00497                         return
00498                     if timeout is not None and time.time() > end_time:
00499                         if timeout > 0:
00500                             raise LockTimeout
00501                         else:
00502                             # Someone else has the lock.
00503                             raise AlreadyLocked
00504                     time.sleep(wait)
00505                 else:
00506                     # Couldn't create the lock for some other reason
00507                     raise LockFailed
00508             else:
00509                 open(self.unique_name, "wb").close()
00510                 return
00511 
00512     def release(self):
00513         if not self.is_locked():
00514             raise NotLocked
00515         elif not os.path.exists(self.unique_name):
00516             raise NotMyLock
00517         os.unlink(self.unique_name)
00518         os.rmdir(self.lock_file)
00519 
00520     def is_locked(self):
00521         return os.path.exists(self.lock_file)
00522 
00523     def i_am_locking(self):
00524         return (self.is_locked() and
00525                 os.path.exists(self.unique_name))
00526 
00527     def break_lock(self):
00528         if os.path.exists(self.lock_file):
00529             for name in os.listdir(self.lock_file):
00530                 os.unlink(os.path.join(self.lock_file, name))
00531             os.rmdir(self.lock_file)
00532 
00533 class SQLiteFileLock(LockBase):
00534     "Demonstration of using same SQL-based locking."
00535 
00536     import tempfile
00537     _fd, testdb = tempfile.mkstemp()
00538     os.close(_fd)
00539     os.unlink(testdb)
00540     del _fd, tempfile
00541 
00542     def __init__(self, path, threaded=True):
00543         LockBase.__init__(self, path, threaded)
00544         self.lock_file = unicode(self.lock_file)
00545         self.unique_name = unicode(self.unique_name)
00546 
00547         import sqlite3
00548         self.connection = sqlite3.connect(SQLiteFileLock.testdb)
00549         
00550         c = self.connection.cursor()
00551         try:
00552             c.execute("create table locks"
00553                       "("
00554                       "   lock_file varchar(32),"
00555                       "   unique_name varchar(32)"
00556                       ")")
00557         except sqlite3.OperationalError:
00558             pass
00559         else:
00560             self.connection.commit()
00561             import atexit
00562             atexit.register(os.unlink, SQLiteFileLock.testdb)
00563 
00564     def acquire(self, timeout=None):
00565         end_time = time.time()
00566         if timeout is not None and timeout > 0:
00567             end_time += timeout
00568 
00569         if timeout is None:
00570             wait = 0.1
00571         elif timeout <= 0:
00572             wait = 0
00573         else:
00574             wait = timeout / 10
00575 
00576         cursor = self.connection.cursor()
00577 
00578         while True:
00579             if not self.is_locked():
00580                 # Not locked.  Try to lock it.
00581                 cursor.execute("insert into locks"
00582                                "  (lock_file, unique_name)"
00583                                "  values"
00584                                "  (?, ?)",
00585                                (self.lock_file, self.unique_name))
00586                 self.connection.commit()
00587 
00588                 # Check to see if we are the only lock holder.
00589                 cursor.execute("select * from locks"
00590                                "  where unique_name = ?",
00591                                (self.unique_name,))
00592                 rows = cursor.fetchall()
00593                 if len(rows) > 1:
00594                     # Nope.  Someone else got there.  Remove our lock.
00595                     cursor.execute("delete from locks"
00596                                    "  where unique_name = ?",
00597                                    (self.unique_name,))
00598                     self.connection.commit()
00599                 else:
00600                     # Yup.  We're done, so go home.
00601                     return
00602             else:
00603                 # Check to see if we are the only lock holder.
00604                 cursor.execute("select * from locks"
00605                                "  where unique_name = ?",
00606                                (self.unique_name,))
00607                 rows = cursor.fetchall()
00608                 if len(rows) == 1:
00609                     # We're the locker, so go home.
00610                     return
00611                     
00612             # Maybe we should wait a bit longer.
00613             if timeout is not None and time.time() > end_time:
00614                 if timeout > 0:
00615                     # No more waiting.
00616                     raise LockTimeout
00617                 else:
00618                     # Someone else has the lock and we are impatient..
00619                     raise AlreadyLocked
00620 
00621             # Well, okay.  We'll give it a bit longer.
00622             time.sleep(wait)
00623 
00624     def release(self):
00625         if not self.is_locked():
00626             raise NotLocked
00627         if not self.i_am_locking():
00628             raise NotMyLock, ("locker:", self._who_is_locking(),
00629                               "me:", self.unique_name)
00630         cursor = self.connection.cursor()
00631         cursor.execute("delete from locks"
00632                        "  where unique_name = ?",
00633                        (self.unique_name,))
00634         self.connection.commit()
00635 
00636     def _who_is_locking(self):
00637         cursor = self.connection.cursor()
00638         cursor.execute("select unique_name from locks"
00639                        "  where lock_file = ?",
00640                        (self.lock_file,))
00641         return cursor.fetchone()[0]
00642         
00643     def is_locked(self):
00644         cursor = self.connection.cursor()
00645         cursor.execute("select * from locks"
00646                        "  where lock_file = ?",
00647                        (self.lock_file,))
00648         rows = cursor.fetchall()
00649         return not not rows
00650 
00651     def i_am_locking(self):
00652         cursor = self.connection.cursor()
00653         cursor.execute("select * from locks"
00654                        "  where lock_file = ?"
00655                        "    and unique_name = ?",
00656                        (self.lock_file, self.unique_name))
00657         return not not cursor.fetchall()
00658 
00659     def break_lock(self):
00660         cursor = self.connection.cursor()
00661         cursor.execute("delete from locks"
00662                        "  where lock_file = ?",
00663                        (self.lock_file,))
00664         self.connection.commit()
00665 
00666 if hasattr(os, "link"):
00667     FileLock = LinkFileLock
00668 else:
00669     FileLock = MkdirFileLock
00670 
00671 ##
00672 # Execute func(*args, **kwargs) after dt seconds.
00673 # 
00674 #     Helper for docttests.
00675 #     
00676 def _in_thread(func, *args, **kwargs):
00677     def _f():
00678         func(*args, **kwargs)
00679     t = threading.Thread(target=_f, name='/*/*')
00680     t.start()
00681     return t
00682 
00683 ##
00684 # Return platform-appropriate lock file name.
00685 # 
00686 #     Helper for doctests.
00687 #     
00688 def _testfile():
00689     import tempfile
00690     return os.path.join(tempfile.gettempdir(), 'trash-%s' % os.getpid())
00691 
00692 ##
00693 # Lock from another thread.
00694 # 
00695 #     Helper for doctests.
00696 #     
00697 def _lock_wait_unlock(event1, event2):
00698     lock = FileLock(_testfile())
00699     with lock:
00700         event1.set()  # we're in,
00701         event2.wait() # wait for boss's permission to leave
00702 
00703 def _test():
00704     global FileLock
00705 
00706     import doctest
00707     import sys
00708 
00709     def test_object(c):
00710         nfailed = ntests = 0
00711         for (obj, recurse) in ((c, True),
00712                                (LockBase, True),
00713                                (sys.modules["__main__"], False)):
00714             tests = doctest.DocTestFinder(recurse=recurse).find(obj)
00715             runner = doctest.DocTestRunner(verbose="-v" in sys.argv)
00716             tests.sort(key = lambda test: test.name)
00717             for test in tests:
00718                 f, t = runner.run(test)
00719                 nfailed += f
00720                 ntests += t
00721         print FileLock.__name__, "tests:", ntests, "failed:", nfailed
00722         return nfailed, ntests
00723 
00724     nfailed = ntests = 0
00725 
00726     if hasattr(os, "link"):
00727         FileLock = LinkFileLock
00728         f, t = test_object(FileLock)
00729         nfailed += f
00730         ntests += t
00731 
00732     if hasattr(os, "mkdir"):
00733         FileLock = MkdirFileLock
00734         f, t = test_object(FileLock)
00735         nfailed += f
00736         ntests += t
00737 
00738     try:
00739         import sqlite3
00740     except ImportError:
00741         print "SQLite3 is unavailable - not testing SQLiteFileLock."
00742     else:
00743         print "Testing SQLiteFileLock with sqlite", sqlite3.sqlite_version,
00744         print "& pysqlite", sqlite3.version
00745         FileLock = SQLiteFileLock
00746         f, t = test_object(FileLock)
00747         nfailed += f
00748         ntests += t
00749 
00750     print "total tests:", ntests, "total failed:", nfailed
00751 
00752 if __name__ == "__main__":
00753     _test()
00754 
00755 

© Copyright 2008-2009 Vyper Logix Corp., All Right Reserved; If you reference this document or any part of this document you must use the citation verbatim (including the link) "© Copyright 2008-2009 Vyper Logix Corp., All Right Reserved."

Notice: This source code contained in this document is NOT open source and is NOT being distributed as open source.

122,241 lines of code and growing...