Skip to content

Commit

Permalink
Merge pull request #7 from AntoineCezar/feat_timeout
Browse files Browse the repository at this point in the history
Add timeout for blocking lock
  • Loading branch information
AntoineCezar committed Jan 24, 2016
2 parents 1dbbc61 + 792c530 commit 123f252
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 58 deletions.
9 changes: 8 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ Exclusive blocking lock::
with FlockOpen('/tmp/my.lock', 'w') as lock:
lock.fd.write('Locked\n')

Exclusive blocking lock with 1 second timeout::

from flockcontext import FlockOpen

with FlockOpen('/tmp/my.lock', 'w', timeout=1) as lock:
lock.fd.write('Locked\n')

Exclusive non-blocking lock::

from flockcontext import FlockOpen
Expand Down Expand Up @@ -62,7 +69,7 @@ Acquire and release within context::
print('Lock acquired')
lock.fd.write('Locked\n')

Locking alredy opened file::
Locking already opened file::

from flockcontext import Flock

Expand Down
37 changes: 29 additions & 8 deletions flockcontext/flock.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
import fcntl

from timeoutcontext import timeout


class Flock(object):
"""Locks an opened file.
Expand All @@ -13,6 +15,14 @@ class Flock(object):
>>> with Flock(fd):
>>> lock.fd.write('Locked\n')
Blocking lock with timeout:
>>> from flockcontext import Flock
>>>
>>> with open('/tmp/my.lock', 'w') as fd:
>>> with Flock(fd, timeout=1):
>>> lock.fd.write('Locked\n')
Non blocking lock:
>>> from flockcontext import Flock
Expand Down Expand Up @@ -49,16 +59,23 @@ class Flock(object):
>>> fd.write('Locked\n')
"""

def __init__(self, fd, exclusive=True, blocking=True):
def __init__(self, fd, exclusive=True, blocking=True, timeout=None):
self._fd = fd

if exclusive:
self._op = fcntl.LOCK_EX
self._exclusive = exclusive
self._blocking = blocking
self._timeout = timeout

@property
def _op(self):
if self._exclusive:
op = fcntl.LOCK_EX
else:
self._op = fcntl.LOCK_SH
op = fcntl.LOCK_SH

if not blocking:
self._op = self._op | fcntl.LOCK_NB
if not self._blocking:
op = op | fcntl.LOCK_NB

return op

def __enter__(self):
self.acquire()
Expand All @@ -68,7 +85,11 @@ def __exit__(self, exec_type, exec_val, exec_tb):
self.release()

def acquire(self):
fcntl.flock(self._fd, self._op)
if self._blocking:
with timeout(self._timeout):
fcntl.flock(self._fd, self._op)
else:
fcntl.flock(self._fd, self._op)

def release(self):
fcntl.flock(self._fd, fcntl.LOCK_UN)
7 changes: 7 additions & 0 deletions flockcontext/flock_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ class FlockOpen(object):
>>> with FlockOpen('/tmp/my.lock', 'w') as lock:
>>> lock.fd.write('Locked\n')
Blocking lock wih timeout exemple:
>>> from flockcontext import FlockOpen
>>>
>>> with FlockOpen('/tmp/my.lock', 'w', timeout=1) as lock:
>>> lock.fd.write('Locked\n')
Non blocking lock exemple:
>>> from flockcontext import FlockOpen
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
history = history_file.read().replace('.. :changelog:', '')

requirements = [
'timeoutcontext',
]

test_requirements = [
Expand Down
26 changes: 17 additions & 9 deletions tests/flock_testcase.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,15 @@

from flockcontext import Flock


LOCK_EX_NB = fcntl.LOCK_EX | fcntl.LOCK_NB


class FlockTestCase(unittest.TestCase):

def setUp(self):
_, path = tempfile.mkstemp()
self.lockfile = path
self.addCleanup(os.remove, path)
handle, path = self.mkstemp()
self.lockfile_handle = handle
self.lockfile_path = path

def assertLocked(self, filepath):
with self.assertRaises(IOError):
Expand All @@ -38,10 +37,19 @@ def assertUnlocked(self, filepath):
except IOError:
self.fail('%s is locked.' % lockfile)

def exclusive_lock(self, filepath):
fd = open(filepath, 'w')
def mkstemp(self):
handle, path = tempfile.mkstemp()
self.addCleanup(os.remove, path)

return handle, path

def lock(self, path):
fd = self.open(path)
fcntl.flock(fd, LOCK_EX_NB)
return fd
self.addCleanup(fcntl.flock, fd, fcntl.LOCK_UN)

def unlock(self, fd):
fcntl.flock(fd, fcntl.LOCK_UN)
def open(self, path, mode='r'):
fd = open(path, mode)
self.addCleanup(fd.close)

return fd
61 changes: 30 additions & 31 deletions tests/test_flock.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
test_flock
----------------------------------
Tests for `flock` module.
"""

import unittest

from flockcontext import Flock
from timeoutcontext import timeout, TimeoutException

from .flock_testcase import FlockTestCase

Expand All @@ -19,40 +13,45 @@
class TestFlock(FlockTestCase):

def test_file_is_locked_within_context(self):
with open(self.lockfile, 'w') as fd:
with Flock(fd) as lock:
self.assertLocked(self.lockfile)
with Flock(self.lockfile_handle) as lock:
self.assertLocked(self.lockfile_path)

def test_file_is_unlocked_after_context(self):
with open(self.lockfile, 'w') as fd:
with Flock(fd) as lock:
pass
with Flock(self.lockfile_handle) as lock:
pass

self.assertUnlocked(self.lockfile)
self.assertUnlocked(self.lockfile_path)

def test_non_blocking_does_not_wait_for_lock(self):
locked_fd = self.exclusive_lock(self.lockfile)
self.lock(self.lockfile_path)

with self.assertRaises(IOError):
with open(self.lockfile, 'w') as fd:
with Flock(fd, blocking=False) as lock:
pass

self.unlock(locked_fd)
with Flock(self.lockfile_handle, blocking=False) as lock:
pass

def test_it_can_be_released_within_context(self):
with open(self.lockfile, 'w') as fd:
with Flock(fd) as lock:
lock.release()
self.assertUnlocked(self.lockfile)

with Flock(self.lockfile_handle) as lock:
lock.release()
self.assertUnlocked(self.lockfile_path)

def test_it_can_be_acquired_within_context(self):
with open(self.lockfile, 'w') as fd:
with Flock(fd) as lock:
lock.release()
lock.acquire()
self.assertLocked(self.lockfile)

with Flock(self.lockfile_handle) as lock:
lock.release()
lock.acquire()
self.assertLocked(self.lockfile_path)

if __name__ == '__main__':
unittest.main()
def test_it_raise_timeouterror_if_timeout_is_reached(self):
self.lock(self.lockfile_path)

with self.assertRaises(TimeoutException):
with Flock(self.lockfile_handle, timeout=1) as lock:
pass

def test_timeout_is_ignored_when_not_blocking(self):
self.lock(self.lockfile_path)

with self.assertRaises(IOError):
with Flock(self.lockfile_handle, blocking=False, timeout=1) as lock:
pass
14 changes: 5 additions & 9 deletions tests/test_flock_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,16 @@
class TestFlockOpen(FlockTestCase):

def test_it_as_a_file_descriptor_object(self):
with FlockOpen(self.lockfile, 'w') as lock:
with FlockOpen(self.lockfile_path, 'w') as lock:
self.assertIsInstance(lock.fd, TextIOWrapper)

def test_it_can_be_unlocked_within_context(self):
with FlockOpen(self.lockfile, 'w') as lock:
with FlockOpen(self.lockfile_path, 'w') as lock:
lock.release()
self.assertUnlocked(self.lockfile)
self.assertUnlocked(self.lockfile_path)

def test_it_can_be_acquired_within_context(self):
with FlockOpen(self.lockfile, 'w') as lock:
with FlockOpen(self.lockfile_path, 'w') as lock:
lock.release()
lock.acquire()
self.assertLocked(self.lockfile)


if __name__ == '__main__':
unittest.main()
self.assertLocked(self.lockfile_path)

0 comments on commit 123f252

Please sign in to comment.