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...