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
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']
76
class Error(Exception):
77
"""Base class for other exceptions.
88
class LockError(Error):
89
"""Base class for error arising from attempts to acquire the lock.
100
class LockTimeout(LockError):
101
"""Raised when lock creation fails within a user-defined period of time.
104
... raise LockTimeout
105
... except LockError:
111
class AlreadyLocked(LockError):
112
"""Some other thread/process is locking the file.
115
... raise AlreadyLocked
116
... except LockError:
123
class LockFailed(LockError):
124
"""Lock file creation failed for some other reason.
128
... except LockError:
135
class UnlockError(Error):
136
"""Base class for errors arising from attempts to release the lock.
139
... raise UnlockError
147
class NotLocked(UnlockError):
148
"""Raised when an attempt is made to unlock an unlocked file.
152
... except UnlockError:
159
class NotMyLock(UnlockError):
160
"""Raised when an attempt is made to unlock a file someone else locked.
164
... except UnlockError:
172
"""Base class for platform-specific lock classes."""
174
def __init__(self, path, threaded=True):
176
>>> lock = LockBase('somefile')
177
>>> lock = LockBase('somefile', threaded=False)
180
self.lock_file = os.path.abspath(path) + '.lock'
181
self.hostname = socket.gethostname()
182
self.pid = os.getpid()
184
tname = '%s-' % threading.current_thread().get_name()
187
dirname = os.path.dirname(self.lock_file)
188
self.unique_name = os.path.join(dirname,
189
'%s.%s%s' % (self.hostname,
193
def acquire(self, timeout=None):
196
* If timeout is omitted (or None), wait forever trying to lock the
199
* If timeout > 0, try to acquire the lock for that many seconds. If
200
the lock period expires and the file is still locked, raise
203
* If timeout <= 0, raise AlreadyLocked immediately if the file is
207
raise NotImplemented('implement in subclass')
212
If the file is not locked, raise NotLocked.
215
raise NotImplemented('implement in subclass')
218
"""Tell whether or not the file is locked."""
219
raise NotImplemented('implement in subclass')
221
def i_am_locking(self):
222
"""Return True if this object is locking the file."""
223
raise NotImplemented('implement in subclass')
225
def break_lock(self):
228
Useful if a locking thread failed to unlock.
231
raise NotImplemented('implement in subclass')
234
"""Context manager support."""
238
def __exit__(self, *_exc):
239
"""Context manager support."""
243
class LinkFileLock(LockBase):
244
"""Lock access to a file using atomic property of link(2)."""
246
def acquire(self, timeout=None):
248
open(self.unique_name, 'wb').close()
252
end_time = time.time()
253
if timeout is not None and timeout > 0:
257
# Try and create a hard link to it.
259
os.link(self.unique_name, self.lock_file)
261
# Link creation failed. Maybe we've double-locked?
262
nlinks = os.stat(self.unique_name).st_nlink
264
# The original link plus the one I created == 2. We're
268
# Otherwise the lock creation failed.
269
if timeout is not None and time.time() > end_time:
270
os.unlink(self.unique_name)
275
time.sleep(timeout is not None and timeout / 10 or 0.1)
277
# Link creation succeeded. We're good to go.
281
if not self.is_locked():
283
elif not os.path.exists(self.unique_name):
285
os.unlink(self.unique_name)
286
os.unlink(self.lock_file)
289
return os.path.exists(self.lock_file)
291
def i_am_locking(self):
292
return (self.is_locked() and
293
os.path.exists(self.unique_name) and
294
os.stat(self.unique_name).st_nlink == 2)
296
def break_lock(self):
297
if os.path.exists(self.lock_file):
298
os.unlink(self.lock_file)
301
class MkdirFileLock(LockBase):
302
"""Lock file by creating a directory."""
304
def __init__(self, path, threaded=True):
306
>>> lock = MkdirFileLock('somefile')
307
>>> lock = MkdirFileLock('somefile', threaded=False)
309
LockBase.__init__(self, path, threaded)
311
tname = '%x-' % thread.get_ident()
314
# Lock file itself is a directory. Place the unique file name into
316
self.unique_name = os.path.join(self.lock_file,
317
'%s.%s%s' % (self.hostname,
321
def acquire(self, timeout=None):
322
end_time = time.time()
323
if timeout is not None and timeout > 0:
329
wait = max(0, timeout / 10)
333
os.mkdir(self.lock_file)
335
err = sys.exc_info()[1]
336
if err.errno == errno.EEXIST:
338
if os.path.exists(self.unique_name):
339
# Already locked by me.
341
if timeout is not None and time.time() > end_time:
345
# Someone else has the lock.
349
# Couldn't create the lock for some other reason
352
open(self.unique_name, 'wb').close()
356
if not self.is_locked():
358
elif not os.path.exists(self.unique_name):
360
os.unlink(self.unique_name)
361
os.rmdir(self.lock_file)
364
return os.path.exists(self.lock_file)
366
def i_am_locking(self):
367
return (self.is_locked() and
368
os.path.exists(self.unique_name))
370
def break_lock(self):
371
if os.path.exists(self.lock_file):
372
for name in os.listdir(self.lock_file):
373
os.unlink(os.path.join(self.lock_file, name))
374
os.rmdir(self.lock_file)
377
class SQLiteFileLock(LockBase):
378
'Demonstration of using same SQL-based locking.'
381
_fd, testdb = tempfile.mkstemp()
386
def __init__(self, path, threaded=True):
387
LockBase.__init__(self, path, threaded)
388
self.lock_file = str(self.lock_file)
389
self.unique_name = str(self.unique_name)
392
self.connection = sqlite3.connect(SQLiteFileLock.testdb)
394
c = self.connection.cursor()
396
c.execute('create table locks'
398
' lock_file varchar(32),'
399
' unique_name varchar(32)'
401
except sqlite3.OperationalError:
404
self.connection.commit()
406
atexit.register(os.unlink, SQLiteFileLock.testdb)
408
def acquire(self, timeout=None):
409
end_time = time.time()
410
if timeout is not None and timeout > 0:
420
cursor = self.connection.cursor()
423
if not self.is_locked():
424
# Not locked. Try to lock it.
425
cursor.execute('insert into locks'
426
' (lock_file, unique_name)'
429
(self.lock_file, self.unique_name))
430
self.connection.commit()
432
# Check to see if we are the only lock holder.
433
cursor.execute('select * from locks'
434
' where unique_name = ?',
436
rows = cursor.fetchall()
438
# Nope. Someone else got there. Remove our lock.
439
cursor.execute('delete from locks'
440
' where unique_name = ?',
442
self.connection.commit()
444
# Yup. We're done, so go home.
447
# Check to see if we are the only lock holder.
448
cursor.execute('select * from locks'
449
' where unique_name = ?',
451
rows = cursor.fetchall()
453
# We're the locker, so go home.
456
# Maybe we should wait a bit longer.
457
if timeout is not None and time.time() > end_time:
462
# Someone else has the lock and we are impatient..
465
# Well, okay. We'll give it a bit longer.
469
if not self.is_locked():
471
if not self.i_am_locking():
472
raise NotMyLock((self._who_is_locking(), self.unique_name))
473
cursor = self.connection.cursor()
474
cursor.execute('delete from locks'
475
' where unique_name = ?',
477
self.connection.commit()
479
def _who_is_locking(self):
480
cursor = self.connection.cursor()
481
cursor.execute('select unique_name from locks'
482
' where lock_file = ?',
484
return cursor.fetchone()[0]
487
cursor = self.connection.cursor()
488
cursor.execute('select * from locks'
489
' where lock_file = ?',
491
rows = cursor.fetchall()
494
def i_am_locking(self):
495
cursor = self.connection.cursor()
496
cursor.execute('select * from locks'
497
' where lock_file = ?'
498
' and unique_name = ?',
499
(self.lock_file, self.unique_name))
500
return not not cursor.fetchall()
502
def break_lock(self):
503
cursor = self.connection.cursor()
504
cursor.execute('delete from locks'
505
' where lock_file = ?',
507
self.connection.commit()
509
if hasattr(os, 'link'):
510
FileLock = LinkFileLock
512
FileLock = MkdirFileLock