Jason R. Coombs avatar Jason R. Coombs committed 5c4a692

Copied implementation from eggmonster

Comments (0)

Files changed (3)

yg/lockfile/__init__.py

+#!/usr/bin/env python
+
+"""
+Cross-platform file lock context
+"""
+
+import os
+import time
+import functools
+
+import zc.lockfile
+
+class FileLockTimeout(Exception):
+    pass
+
+class FileLock(object):
+    """
+    A cross-platform locking file context.
+
+    May be used in a with statement to provide system-level concurrency
+    protection.
+
+    This class relies on zc.lockfile for the underlying locking.
+
+    This class is not threadsafe.
+    """
+
+    def __init__(self, lockfile, timeout=10, delay=.05):
+        """
+        Construct a FileLock. Specify the file to lock and optionally
+        the maximum timeout and the delay between each attempt to lock.
+        """
+        self.lockfile = lockfile
+        self.timeout = timeout
+        self.delay = delay
+
+    def acquire(self):
+        """
+        Attempt to acquire the lock every `delay` seconds until the
+        lock is acquired or until `timeout` has expired.
+
+        Raises FileLockTimeout if the timeout is exceeded.
+
+        Errors opening the lock file (other than if it exists) are
+        passed through.
+        """
+        start_time = time.time()
+        attempt = functools.partial(zc.lockfile.LockFile, self.lockfile)
+        while True:
+            try:
+                self.lock = attempt()
+                break
+            except zc.lockfile.LockError:
+                timeout_expired = time.time()-start_time >= self.timeout
+                if timeout_expired:
+                    raise FileLockTimeout()
+                time.sleep(self.delay)
+
+    def is_locked(self):
+        return hasattr(self, 'lock')
+
+    def release(self):
+        """
+        Release the lock and delete the lockfile.
+        """
+        if self.is_locked():
+            self.lock.close()
+            del self.lock
+            os.remove(self.lockfile)
+
+    def __enter__(self):
+        """
+        Acquire the lock unless we already have it.
+        """
+        if not self.is_locked():
+            self.acquire()
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.release()
+
+    def __del__(self):
+        """
+        Release the lock on destruction.
+        """
+        self.release()
Add a comment to this file

yg/lockfile/test/__init__.py

Empty file added.

yg/lockfile/test/test_lockfile.py

+from __future__ import with_statement
+
+import os
+import subprocess
+import itertools
+import tempfile
+import time
+import sys
+
+import py.test
+
+from yg.lockfile import FileLock, FileLockTimeout
+
+def test_FileLock_basic():
+    tfile, filename = tempfile.mkstemp()
+    os.close(tfile)
+    os.remove(filename)
+    l = FileLock(filename)
+    l2 = FileLock(filename, timeout=0.2)
+    assert not l.is_locked()
+    l.acquire()
+    assert l.is_locked()
+    l.release()
+    assert not l.is_locked()
+    with l:
+        assert os.path.isfile(filename)
+        py.test.raises(FileLockTimeout, l2.acquire)
+    assert not l.is_locked()
+    l2.acquire()
+    assert l2.is_locked()
+    l2.release()
+
+def lines(stream):
+    """
+    I can't figure out how to get the subprocess module to feed me
+    line-buffered output from a sub-process, so I grab the output byte
+    by byte and assemble it into lines.
+    """
+    buf = ''
+    while True:
+        dat = stream.read(1)
+        if dat:
+            buf += dat
+            if dat == '\n':
+                yield buf
+                buf = ''
+        if not dat and buf:
+            yield buf
+        if not dat:
+            break
+
+def test_FileLock_process_killed():
+    """
+    If a subprocess fails to release the lock, it should be released
+    and available for another process to take it.
+    """
+    tfile, filename = tempfile.mkstemp()
+    os.close(tfile)
+    os.remove(filename)
+    cmd = [sys.executable, '-u', '-c', 'from eggmonster.concurrency '
+        'import FileLock; import time; l = FileLock(%(filename)r); '
+        'l.acquire(); print "acquired", l.lockfile; '
+        '[time.sleep(1) for x in xrange(10)]' % vars()]
+    proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
+    out = itertools.takewhile(lambda l: 'acquired' not in l,
+        lines(proc.stdout))
+    tuple(out) # wait for 'acquired' to be printed by subprocess
+
+    l = FileLock(filename, timeout=0.2)
+    py.test.raises(FileLockTimeout, l.acquire)
+    proc.kill()
+    time.sleep(.5)
+    l.acquire(); l.release()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.