~widelands-dev/widelands-website/django_staticfiles

« back to all changes in this revision

Viewing changes to notification/lockfile.py

  • Committer: franku
  • Date: 2016-07-02 12:38:06 UTC
  • mfrom: (404.2.56 widelands)
  • Revision ID: somal@arcor.de-20160702123806-q69u3d48s1prrxds
merged the django1_8 branch

Show diffs side-by-side

added added

removed removed

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