3
lockfile.py - Platform-independent advisory file locks.
5
Requires Python 2.5 unless you apply 2.4.diff
6
Locking is done on a per-thread basis instead of a per-process basis.
10
>>> lock = FileLock('somefile')
13
... except AlreadyLocked:
14
... print 'somefile', 'is locked already.'
15
... except LockFailed:
16
... print 'somefile', 'can\\'t be locked.'
20
>>> print lock.is_locked()
24
>>> lock = FileLock('somefile')
25
>>> print lock.is_locked()
28
... print lock.is_locked()
30
>>> print lock.is_locked()
32
>>> # It is okay to lock twice from the same thread...
36
>>> # Though no counter is kept, so you can't unlock multiple times...
37
>>> print lock.is_locked()
42
Error - base class for other exceptions
43
LockError - base class for all locking exceptions
44
AlreadyLocked - Another thread or process already holds the lock
45
LockFailed - Lock failed for some other reason
46
UnlockError - base class for all unlocking exceptions
47
AlreadyUnlocked - File was not locked.
48
NotMyLock - File was locked but not by the current thread/process
51
from __future__ import division
60
# Work with PEP8 and non-PEP8 versions of threading module.
62
threading.current_thread
63
except AttributeError:
64
threading.current_thread = threading.currentThread
66
# python 2.6 has threading.current_thread so we need to do this separately.
67
threading.Thread.get_name
68
except AttributeError:
69
threading.Thread.get_name = threading.Thread.getName
71
__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
72
'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
73
'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
75
class Error(Exception):
77
Base class for other exceptions.
86
class LockError(Error):
88
Base class for error arising from attempts to acquire the lock.
97
class LockTimeout(LockError):
98
"""Raised when lock creation fails within a user-defined period of time.
101
... raise LockTimeout
102
... except LockError:
107
class AlreadyLocked(LockError):
108
"""Some other thread/process is locking the file.
111
... raise AlreadyLocked
112
... except LockError:
117
class LockFailed(LockError):
118
"""Lock file creation failed for some other reason.
122
... except LockError:
127
class UnlockError(Error):
129
Base class for errors arising from attempts to release the lock.
132
... raise UnlockError
138
class NotLocked(UnlockError):
139
"""Raised when an attempt is made to unlock an unlocked file.
143
... except UnlockError:
148
class NotMyLock(UnlockError):
149
"""Raised when an attempt is made to unlock a file someone else locked.
153
... except UnlockError:
159
"""Base class for platform-specific lock classes."""
160
def __init__(self, path, threaded=True):
162
>>> lock = LockBase('somefile')
163
>>> lock = LockBase('somefile', threaded=False)
166
self.lock_file = os.path.abspath(path) + ".lock"
167
self.hostname = socket.gethostname()
168
self.pid = os.getpid()
170
tname = "%s-" % threading.current_thread().get_name()
173
dirname = os.path.dirname(self.lock_file)
174
self.unique_name = os.path.join(dirname,
175
"%s.%s%s" % (self.hostname,
179
def acquire(self, timeout=None):
183
* If timeout is omitted (or None), wait forever trying to lock the
186
* If timeout > 0, try to acquire the lock for that many seconds. If
187
the lock period expires and the file is still locked, raise
190
* If timeout <= 0, raise AlreadyLocked immediately if the file is
193
raise NotImplemented("implement in subclass")
199
If the file is not locked, raise NotLocked.
201
raise NotImplemented("implement in subclass")
205
Tell whether or not the file is locked.
207
raise NotImplemented("implement in subclass")
209
def i_am_locking(self):
211
Return True if this object is locking the file.
213
raise NotImplemented("implement in subclass")
215
def break_lock(self):
217
Remove a lock. Useful if a locking thread failed to unlock.
219
raise NotImplemented("implement in subclass")
223
Context manager support.
228
def __exit__(self, *_exc):
230
Context manager support.
234
class LinkFileLock(LockBase):
235
"""Lock access to a file using atomic property of link(2)."""
237
def acquire(self, timeout=None):
239
open(self.unique_name, "wb").close()
243
end_time = time.time()
244
if timeout is not None and timeout > 0:
248
# Try and create a hard link to it.
250
os.link(self.unique_name, self.lock_file)
252
# Link creation failed. Maybe we've double-locked?
253
nlinks = os.stat(self.unique_name).st_nlink
255
# The original link plus the one I created == 2. We're
259
# Otherwise the lock creation failed.
260
if timeout is not None and time.time() > end_time:
261
os.unlink(self.unique_name)
266
time.sleep(timeout is not None and timeout/10 or 0.1)
268
# Link creation succeeded. We're good to go.
272
if not self.is_locked():
274
elif not os.path.exists(self.unique_name):
276
os.unlink(self.unique_name)
277
os.unlink(self.lock_file)
280
return os.path.exists(self.lock_file)
282
def i_am_locking(self):
283
return (self.is_locked() and
284
os.path.exists(self.unique_name) and
285
os.stat(self.unique_name).st_nlink == 2)
287
def break_lock(self):
288
if os.path.exists(self.lock_file):
289
os.unlink(self.lock_file)
291
class MkdirFileLock(LockBase):
292
"""Lock file by creating a directory."""
293
def __init__(self, path, threaded=True):
295
>>> lock = MkdirFileLock('somefile')
296
>>> lock = MkdirFileLock('somefile', threaded=False)
298
LockBase.__init__(self, path, threaded)
300
tname = "%x-" % thread.get_ident()
303
# Lock file itself is a directory. Place the unique file name into
305
self.unique_name = os.path.join(self.lock_file,
306
"%s.%s%s" % (self.hostname,
310
def acquire(self, timeout=None):
311
end_time = time.time()
312
if timeout is not None and timeout > 0:
318
wait = max(0, timeout / 10)
322
os.mkdir(self.lock_file)
324
err = sys.exc_info()[1]
325
if err.errno == errno.EEXIST:
327
if os.path.exists(self.unique_name):
328
# Already locked by me.
330
if timeout is not None and time.time() > end_time:
334
# Someone else has the lock.
338
# Couldn't create the lock for some other reason
341
open(self.unique_name, "wb").close()
345
if not self.is_locked():
347
elif not os.path.exists(self.unique_name):
349
os.unlink(self.unique_name)
350
os.rmdir(self.lock_file)
353
return os.path.exists(self.lock_file)
355
def i_am_locking(self):
356
return (self.is_locked() and
357
os.path.exists(self.unique_name))
359
def break_lock(self):
360
if os.path.exists(self.lock_file):
361
for name in os.listdir(self.lock_file):
362
os.unlink(os.path.join(self.lock_file, name))
363
os.rmdir(self.lock_file)
365
class SQLiteFileLock(LockBase):
366
"Demonstration of using same SQL-based locking."
369
_fd, testdb = tempfile.mkstemp()
374
def __init__(self, path, threaded=True):
375
LockBase.__init__(self, path, threaded)
376
self.lock_file = unicode(self.lock_file)
377
self.unique_name = unicode(self.unique_name)
380
self.connection = sqlite3.connect(SQLiteFileLock.testdb)
382
c = self.connection.cursor()
384
c.execute("create table locks"
386
" lock_file varchar(32),"
387
" unique_name varchar(32)"
389
except sqlite3.OperationalError:
392
self.connection.commit()
394
atexit.register(os.unlink, SQLiteFileLock.testdb)
396
def acquire(self, timeout=None):
397
end_time = time.time()
398
if timeout is not None and timeout > 0:
408
cursor = self.connection.cursor()
411
if not self.is_locked():
412
# Not locked. Try to lock it.
413
cursor.execute("insert into locks"
414
" (lock_file, unique_name)"
417
(self.lock_file, self.unique_name))
418
self.connection.commit()
420
# Check to see if we are the only lock holder.
421
cursor.execute("select * from locks"
422
" where unique_name = ?",
424
rows = cursor.fetchall()
426
# Nope. Someone else got there. Remove our lock.
427
cursor.execute("delete from locks"
428
" where unique_name = ?",
430
self.connection.commit()
432
# Yup. We're done, so go home.
435
# Check to see if we are the only lock holder.
436
cursor.execute("select * from locks"
437
" where unique_name = ?",
439
rows = cursor.fetchall()
441
# We're the locker, so go home.
444
# Maybe we should wait a bit longer.
445
if timeout is not None and time.time() > end_time:
450
# Someone else has the lock and we are impatient..
453
# Well, okay. We'll give it a bit longer.
457
if not self.is_locked():
459
if not self.i_am_locking():
460
raise NotMyLock((self._who_is_locking(), self.unique_name))
461
cursor = self.connection.cursor()
462
cursor.execute("delete from locks"
463
" where unique_name = ?",
465
self.connection.commit()
467
def _who_is_locking(self):
468
cursor = self.connection.cursor()
469
cursor.execute("select unique_name from locks"
470
" where lock_file = ?",
472
return cursor.fetchone()[0]
475
cursor = self.connection.cursor()
476
cursor.execute("select * from locks"
477
" where lock_file = ?",
479
rows = cursor.fetchall()
482
def i_am_locking(self):
483
cursor = self.connection.cursor()
484
cursor.execute("select * from locks"
485
" where lock_file = ?"
486
" and unique_name = ?",
487
(self.lock_file, self.unique_name))
488
return not not cursor.fetchall()
490
def break_lock(self):
491
cursor = self.connection.cursor()
492
cursor.execute("delete from locks"
493
" where lock_file = ?",
495
self.connection.commit()
497
if hasattr(os, "link"):
498
FileLock = LinkFileLock
500
FileLock = MkdirFileLock