blob: bca686cff82a183b6941899f7feecad292994d51 [file] [log] [blame]
Ilja H. Friedel711fcdb2018-01-22 22:08:00 -08001
2"""
3lockfile.py - Platform-independent advisory file locks.
4
5Requires Python 2.5 unless you apply 2.4.diff
6Locking is done on a per-thread basis instead of a per-process basis.
7
8Usage:
9
10>>> lock = FileLock('somefile')
11>>> try:
12... lock.acquire()
13... except AlreadyLocked:
14... print 'somefile', 'is locked already.'
15... except LockFailed:
16... print 'somefile', 'can\\'t be locked.'
17... else:
18... print 'got lock'
19got lock
20>>> print lock.is_locked()
21True
22>>> lock.release()
23
24>>> lock = FileLock('somefile')
25>>> print lock.is_locked()
26False
27>>> with lock:
28... print lock.is_locked()
29True
30>>> print lock.is_locked()
31False
32>>> # It is okay to lock twice from the same thread...
33>>> with lock:
34... lock.acquire()
35...
36>>> # Though no counter is kept, so you can't unlock multiple times...
37>>> print lock.is_locked()
38False
39
40Exceptions:
41
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
49"""
50
51from __future__ import division
52
53import sys
54import socket
55import os
56import thread
57import threading
58import time
59import errno
60import urllib
61
62# Work with PEP8 and non-PEP8 versions of threading module.
63if not hasattr(threading, "current_thread"):
64 threading.current_thread = threading.currentThread
65if not hasattr(threading.Thread, "get_name"):
66 threading.Thread.get_name = threading.Thread.getName
67
68__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked',
69 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock',
70 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock']
71
72class Error(Exception):
73 """
74 Base class for other exceptions.
75
76 >>> try:
77 ... raise Error
78 ... except Exception:
79 ... pass
80 """
81 pass
82
83class LockError(Error):
84 """
85 Base class for error arising from attempts to acquire the lock.
86
87 >>> try:
88 ... raise LockError
89 ... except Error:
90 ... pass
91 """
92 pass
93
94class LockTimeout(LockError):
95 """Raised when lock creation fails within a user-defined period of time.
96
97 >>> try:
98 ... raise LockTimeout
99 ... except LockError:
100 ... pass
101 """
102 pass
103
104class AlreadyLocked(LockError):
105 """Some other thread/process is locking the file.
106
107 >>> try:
108 ... raise AlreadyLocked
109 ... except LockError:
110 ... pass
111 """
112 pass
113
114class LockFailed(LockError):
115 """Lock file creation failed for some other reason.
116
117 >>> try:
118 ... raise LockFailed
119 ... except LockError:
120 ... pass
121 """
122 pass
123
124class UnlockError(Error):
125 """
126 Base class for errors arising from attempts to release the lock.
127
128 >>> try:
129 ... raise UnlockError
130 ... except Error:
131 ... pass
132 """
133 pass
134
135class NotLocked(UnlockError):
136 """Raised when an attempt is made to unlock an unlocked file.
137
138 >>> try:
139 ... raise NotLocked
140 ... except UnlockError:
141 ... pass
142 """
143 pass
144
145class NotMyLock(UnlockError):
146 """Raised when an attempt is made to unlock a file someone else locked.
147
148 >>> try:
149 ... raise NotMyLock
150 ... except UnlockError:
151 ... pass
152 """
153 pass
154
155class LockBase:
156 """Base class for platform-specific lock classes."""
157 def __init__(self, path, threaded=True):
158 """
159 >>> lock = LockBase('somefile')
160 >>> lock = LockBase('somefile', threaded=False)
161 """
162 self.path = path
163 self.lock_file = os.path.abspath(path) + ".lock"
164 self.hostname = socket.gethostname()
165 self.pid = os.getpid()
166 if threaded:
167 name = threading.current_thread().get_name()
168 tname = "%s-" % urllib.quote(name, safe="")
169 else:
170 tname = ""
171 dirname = os.path.dirname(self.lock_file)
172 self.unique_name = os.path.join(dirname,
173 "%s.%s%s" % (self.hostname,
174 tname,
175 self.pid))
176
177 def acquire(self, timeout=None):
178 """
179 Acquire the lock.
180
181 * If timeout is omitted (or None), wait forever trying to lock the
182 file.
183
184 * If timeout > 0, try to acquire the lock for that many seconds. If
185 the lock period expires and the file is still locked, raise
186 LockTimeout.
187
188 * If timeout <= 0, raise AlreadyLocked immediately if the file is
189 already locked.
190 """
191 raise NotImplemented("implement in subclass")
192
193 def release(self):
194 """
195 Release the lock.
196
197 If the file is not locked, raise NotLocked.
198 """
199 raise NotImplemented("implement in subclass")
200
201 def is_locked(self):
202 """
203 Tell whether or not the file is locked.
204 """
205 raise NotImplemented("implement in subclass")
206
207 def i_am_locking(self):
208 """
209 Return True if this object is locking the file.
210 """
211 raise NotImplemented("implement in subclass")
212
213 def break_lock(self):
214 """
215 Remove a lock. Useful if a locking thread failed to unlock.
216 """
217 raise NotImplemented("implement in subclass")
218
219 def __enter__(self):
220 """
221 Context manager support.
222 """
223 self.acquire()
224 return self
225
226 def __exit__(self, *_exc):
227 """
228 Context manager support.
229 """
230 self.release()
231
232class LinkFileLock(LockBase):
233 """Lock access to a file using atomic property of link(2)."""
234
235 def acquire(self, timeout=None):
236 try:
237 open(self.unique_name, "wb").close()
238 except IOError:
239 raise LockFailed("failed to create %s" % self.unique_name)
240
241 end_time = time.time()
242 if timeout is not None and timeout > 0:
243 end_time += timeout
244
245 while True:
246 # Try and create a hard link to it.
247 try:
248 os.link(self.unique_name, self.lock_file)
249 except OSError:
250 # Link creation failed. Maybe we've double-locked?
251 nlinks = os.stat(self.unique_name).st_nlink
252 if nlinks == 2:
253 # The original link plus the one I created == 2. We're
254 # good to go.
255 return
256 else:
257 # Otherwise the lock creation failed.
258 if timeout is not None and time.time() > end_time:
259 os.unlink(self.unique_name)
260 if timeout > 0:
261 raise LockTimeout
262 else:
263 raise AlreadyLocked
264 time.sleep(timeout is not None and timeout/10 or 0.1)
265 else:
266 # Link creation succeeded. We're good to go.
267 return
268
269 def release(self):
270 if not self.is_locked():
271 raise NotLocked
272 elif not os.path.exists(self.unique_name):
273 raise NotMyLock
274 os.unlink(self.unique_name)
275 os.unlink(self.lock_file)
276
277 def is_locked(self):
278 return os.path.exists(self.lock_file)
279
280 def i_am_locking(self):
281 return (self.is_locked() and
282 os.path.exists(self.unique_name) and
283 os.stat(self.unique_name).st_nlink == 2)
284
285 def break_lock(self):
286 if os.path.exists(self.lock_file):
287 os.unlink(self.lock_file)
288
289class MkdirFileLock(LockBase):
290 """Lock file by creating a directory."""
291 def __init__(self, path, threaded=True):
292 """
293 >>> lock = MkdirFileLock('somefile')
294 >>> lock = MkdirFileLock('somefile', threaded=False)
295 """
296 LockBase.__init__(self, path, threaded)
297 if threaded:
298 tname = "%x-" % thread.get_ident()
299 else:
300 tname = ""
301 # Lock file itself is a directory. Place the unique file name into
302 # it.
303 self.unique_name = os.path.join(self.lock_file,
304 "%s.%s%s" % (self.hostname,
305 tname,
306 self.pid))
307
308 def acquire(self, timeout=None):
309 end_time = time.time()
310 if timeout is not None and timeout > 0:
311 end_time += timeout
312
313 if timeout is None:
314 wait = 0.1
315 else:
316 wait = max(0, timeout / 10)
317
318 while True:
319 try:
320 os.mkdir(self.lock_file)
321 except OSError:
322 err = sys.exc_info()[1]
323 if err.errno == errno.EEXIST:
324 # Already locked.
325 if os.path.exists(self.unique_name):
326 # Already locked by me.
327 return
328 if timeout is not None and time.time() > end_time:
329 if timeout > 0:
330 raise LockTimeout
331 else:
332 # Someone else has the lock.
333 raise AlreadyLocked
334 time.sleep(wait)
335 else:
336 # Couldn't create the lock for some other reason
337 raise LockFailed("failed to create %s" % self.lock_file)
338 else:
339 open(self.unique_name, "wb").close()
340 return
341
342 def release(self):
343 if not self.is_locked():
344 raise NotLocked
345 elif not os.path.exists(self.unique_name):
346 raise NotMyLock
347 os.unlink(self.unique_name)
348 os.rmdir(self.lock_file)
349
350 def is_locked(self):
351 return os.path.exists(self.lock_file)
352
353 def i_am_locking(self):
354 return (self.is_locked() and
355 os.path.exists(self.unique_name))
356
357 def break_lock(self):
358 if os.path.exists(self.lock_file):
359 for name in os.listdir(self.lock_file):
360 os.unlink(os.path.join(self.lock_file, name))
361 os.rmdir(self.lock_file)
362
363class SQLiteFileLock(LockBase):
364 "Demonstration of using same SQL-based locking."
365
366 import tempfile
367 _fd, testdb = tempfile.mkstemp()
368 os.close(_fd)
369 os.unlink(testdb)
370 del _fd, tempfile
371
372 def __init__(self, path, threaded=True):
373 LockBase.__init__(self, path, threaded)
374 self.lock_file = unicode(self.lock_file)
375 self.unique_name = unicode(self.unique_name)
376
377 import sqlite3
378 self.connection = sqlite3.connect(SQLiteFileLock.testdb)
379
380 c = self.connection.cursor()
381 try:
382 c.execute("create table locks"
383 "("
384 " lock_file varchar(32),"
385 " unique_name varchar(32)"
386 ")")
387 except sqlite3.OperationalError:
388 pass
389 else:
390 self.connection.commit()
391 import atexit
392 atexit.register(os.unlink, SQLiteFileLock.testdb)
393
394 def acquire(self, timeout=None):
395 end_time = time.time()
396 if timeout is not None and timeout > 0:
397 end_time += timeout
398
399 if timeout is None:
400 wait = 0.1
401 elif timeout <= 0:
402 wait = 0
403 else:
404 wait = timeout / 10
405
406 cursor = self.connection.cursor()
407
408 while True:
409 if not self.is_locked():
410 # Not locked. Try to lock it.
411 cursor.execute("insert into locks"
412 " (lock_file, unique_name)"
413 " values"
414 " (?, ?)",
415 (self.lock_file, self.unique_name))
416 self.connection.commit()
417
418 # Check to see if we are the only lock holder.
419 cursor.execute("select * from locks"
420 " where unique_name = ?",
421 (self.unique_name,))
422 rows = cursor.fetchall()
423 if len(rows) > 1:
424 # Nope. Someone else got there. Remove our lock.
425 cursor.execute("delete from locks"
426 " where unique_name = ?",
427 (self.unique_name,))
428 self.connection.commit()
429 else:
430 # Yup. We're done, so go home.
431 return
432 else:
433 # Check to see if we are the only lock holder.
434 cursor.execute("select * from locks"
435 " where unique_name = ?",
436 (self.unique_name,))
437 rows = cursor.fetchall()
438 if len(rows) == 1:
439 # We're the locker, so go home.
440 return
441
442 # Maybe we should wait a bit longer.
443 if timeout is not None and time.time() > end_time:
444 if timeout > 0:
445 # No more waiting.
446 raise LockTimeout
447 else:
448 # Someone else has the lock and we are impatient..
449 raise AlreadyLocked
450
451 # Well, okay. We'll give it a bit longer.
452 time.sleep(wait)
453
454 def release(self):
455 if not self.is_locked():
456 raise NotLocked
457 if not self.i_am_locking():
458 raise NotMyLock((self._who_is_locking(), self.unique_name))
459 cursor = self.connection.cursor()
460 cursor.execute("delete from locks"
461 " where unique_name = ?",
462 (self.unique_name,))
463 self.connection.commit()
464
465 def _who_is_locking(self):
466 cursor = self.connection.cursor()
467 cursor.execute("select unique_name from locks"
468 " where lock_file = ?",
469 (self.lock_file,))
470 return cursor.fetchone()[0]
471
472 def is_locked(self):
473 cursor = self.connection.cursor()
474 cursor.execute("select * from locks"
475 " where lock_file = ?",
476 (self.lock_file,))
477 rows = cursor.fetchall()
478 return not not rows
479
480 def i_am_locking(self):
481 cursor = self.connection.cursor()
482 cursor.execute("select * from locks"
483 " where lock_file = ?"
484 " and unique_name = ?",
485 (self.lock_file, self.unique_name))
486 return not not cursor.fetchall()
487
488 def break_lock(self):
489 cursor = self.connection.cursor()
490 cursor.execute("delete from locks"
491 " where lock_file = ?",
492 (self.lock_file,))
493 self.connection.commit()
494
495if hasattr(os, "link"):
496 FileLock = LinkFileLock
497else:
498 FileLock = MkdirFileLock