locking code: extract timeout/sleep code into reusable TimeoutTimer class

This commit is contained in:
Thomas Waldmann 2015-07-13 16:45:18 +02:00
parent e4c519b1e9
commit 2deb520e67
2 changed files with 75 additions and 16 deletions

View File

@ -19,6 +19,58 @@ def get_id():
return hostname, pid, tid
class TimeoutTimer:
"""
A timer for timeout checks (can also deal with no timeout, give timeout=None [default]).
It can also compute and optionally execute a reasonable sleep time (e.g. to avoid
polling too often or to support thread/process rescheduling).
"""
def __init__(self, timeout=None, sleep=None):
"""
Initialize a timer.
:param timeout: time out interval [s] or None (no timeout)
:param sleep: sleep interval [s] (>= 0: do sleep call, <0: don't call sleep)
or None (autocompute: use 10% of timeout, or 1s for no timeout)
"""
if timeout is not None and timeout < 0:
raise ValueError("timeout must be >= 0")
self.timeout_interval = timeout
if sleep is None:
if timeout is None:
sleep = 1.0
else:
sleep = timeout / 10.0
self.sleep_interval = sleep
self.start_time = None
self.end_time = None
def __repr__(self):
return "<%s: start=%r end=%r timeout=%r sleep=%r>" % (
self.__class__.__name__, self.start_time, self.end_time,
self.timeout_interval, self.sleep_interval)
def start(self):
self.start_time = time.time()
if self.timeout_interval is not None:
self.end_time = self.start_time + self.timeout_interval
return self
def sleep(self):
if self.sleep_interval >= 0:
time.sleep(self.sleep_interval)
def timed_out(self):
return self.end_time is not None and time.time() >= self.end_time
def timed_out_or_sleep(self):
if self.timed_out():
return True
else:
self.sleep()
return False
class ExclusiveLock:
"""An exclusive Lock based on mkdir fs operation being atomic"""
class LockError(Error):
@ -55,23 +107,12 @@ class ExclusiveLock:
def __repr__(self):
return "<%s: %r>" % (self.__class__.__name__, self.unique_name)
def _get_timing(self, timeout, sleep):
def acquire(self, timeout=None, sleep=None):
if timeout is None:
timeout = self.timeout
start = end = time.time()
if timeout is not None and timeout > 0:
end += timeout
if sleep is None:
sleep = self.sleep
if sleep is None:
if timeout is None:
sleep = 1.0
else:
sleep = max(0, timeout / 10.0)
return start, sleep, end, timeout
def acquire(self, timeout=None, sleep=None):
start, sleep, end, timeout = self._get_timing(timeout, sleep)
timer = TimeoutTimer(timeout, sleep).start()
while True:
try:
os.mkdir(self.path)
@ -79,9 +120,8 @@ class ExclusiveLock:
if err.errno == errno.EEXIST: # already locked
if self.by_me():
return self
if timeout is not None and time.time() > end:
if timer.timed_out_or_sleep():
raise self.LockTimeout(self.path)
time.sleep(sleep)
else:
raise self.LockFailed(self.path, str(err))
else:

View File

@ -1,6 +1,8 @@
import time
import pytest
from ..locking import get_id, ExclusiveLock, UpgradableLock, LockRoster, ADD, REMOVE, SHARED, EXCLUSIVE
from ..locking import get_id, TimeoutTimer, ExclusiveLock , UpgradableLock, LockRoster, ADD, REMOVE, SHARED, EXCLUSIVE
ID1 = "foo", 1, 1
@ -15,6 +17,23 @@ def test_id():
assert pid > 0
class TestTimeoutTimer:
def test_timeout(self):
timeout = 0.5
t = TimeoutTimer(timeout).start()
assert not t.timed_out()
time.sleep(timeout * 1.5)
assert t.timed_out()
def test_notimeout_sleep(self):
timeout, sleep = None, 0.5
t = TimeoutTimer(timeout, sleep).start()
assert not t.timed_out_or_sleep()
assert time.time() >= t.start_time + 1 * sleep
assert not t.timed_out_or_sleep()
assert time.time() >= t.start_time + 2 * sleep
@pytest.fixture()
def lockpath(tmpdir):
return str(tmpdir.join('lock'))