diff --git a/backoff/_async.py b/backoff/_async.py index 14f1415..e9398f2 100644 --- a/backoff/_async.py +++ b/backoff/_async.py @@ -1,8 +1,7 @@ # coding:utf-8 -import datetime +import time import functools import asyncio -from datetime import timedelta from backoff._common import (_init_wait_gen, _maybe_call, _next_wait) @@ -41,6 +40,8 @@ def retry_predicate(target, wait_gen, predicate, *, max_tries, max_time, jitter, on_success, on_backoff, on_giveup, + monotonic_time=None, + sleep=None, wait_gen_kwargs): on_success = _ensure_coroutines(on_success) on_backoff = _ensure_coroutines(on_backoff) @@ -61,11 +62,11 @@ async def retry(*args, **kwargs): max_time = _maybe_call(max_time) tries = 0 - start = datetime.datetime.now() + start = (monotonic_time or time.monotonic)() wait = _init_wait_gen(wait_gen, wait_gen_kwargs) while True: tries += 1 - elapsed = timedelta.total_seconds(datetime.datetime.now() - start) + elapsed = (monotonic_time or time.monotonic)() - start details = { "target": target, "args": args, @@ -102,7 +103,7 @@ async def retry(*args, **kwargs): # See for details: # # - await asyncio.sleep(seconds) + await (sleep or asyncio.sleep)(seconds) continue else: await _call_handlers(on_success, **details, value=ret) @@ -117,6 +118,8 @@ def retry_exception(target, wait_gen, exception, *, max_tries, max_time, jitter, giveup, on_success, on_backoff, on_giveup, raise_on_giveup, + sleep=None, + monotonic_time=None, wait_gen_kwargs): on_success = _ensure_coroutines(on_success) on_backoff = _ensure_coroutines(on_backoff) @@ -136,11 +139,11 @@ async def retry(*args, **kwargs): max_time = _maybe_call(max_time) tries = 0 - start = datetime.datetime.now() + start = (monotonic_time or time.monotonic)() wait = _init_wait_gen(wait_gen, wait_gen_kwargs) while True: tries += 1 - elapsed = timedelta.total_seconds(datetime.datetime.now() - start) + elapsed = (monotonic_time or time.monotonic)() - start details = { "target": target, "args": args, @@ -180,7 +183,7 @@ async def retry(*args, **kwargs): # See for details: # # - await asyncio.sleep(seconds) + await (sleep or asyncio.sleep)(seconds) else: await _call_handlers(on_success, **details) diff --git a/backoff/_decorator.py b/backoff/_decorator.py index 3cb9d94..10da357 100644 --- a/backoff/_decorator.py +++ b/backoff/_decorator.py @@ -33,6 +33,8 @@ def on_predicate(wait_gen: _WaitGenerator, on_success: Union[_Handler, Iterable[_Handler]] = None, on_backoff: Union[_Handler, Iterable[_Handler]] = None, on_giveup: Union[_Handler, Iterable[_Handler]] = None, + monotonic_time: Optional[Callable[[], float]] = None, + sleep: Optional[Callable[[float], None]] = None, logger: _MaybeLogger = 'backoff', backoff_log_level: int = logging.INFO, giveup_log_level: int = logging.ERROR, @@ -113,6 +115,8 @@ def decorate(target): on_success=on_success, on_backoff=on_backoff, on_giveup=on_giveup, + monotonic_time=monotonic_time, + sleep=sleep, wait_gen_kwargs=wait_gen_kwargs ) diff --git a/backoff/_sync.py b/backoff/_sync.py index ecc592d..7052bed 100644 --- a/backoff/_sync.py +++ b/backoff/_sync.py @@ -1,8 +1,6 @@ # coding:utf-8 -import datetime import functools import time -from datetime import timedelta from backoff._common import (_init_wait_gen, _maybe_call, _next_wait) @@ -24,6 +22,8 @@ def retry_predicate(target, wait_gen, predicate, *, max_tries, max_time, jitter, on_success, on_backoff, on_giveup, + monotonic_time=None, + sleep=None, wait_gen_kwargs): @functools.wraps(target) @@ -35,11 +35,11 @@ def retry(*args, **kwargs): max_time = _maybe_call(max_time) tries = 0 - start = datetime.datetime.now() + start = (monotonic_time or time.monotonic)() wait = _init_wait_gen(wait_gen, wait_gen_kwargs) while True: tries += 1 - elapsed = timedelta.total_seconds(datetime.datetime.now() - start) + elapsed = (monotonic_time or time.monotonic)() - start details = { "target": target, "args": args, @@ -67,7 +67,7 @@ def retry(*args, **kwargs): _call_handlers(on_backoff, **details, value=ret, wait=seconds) - time.sleep(seconds) + (sleep or time.sleep)(seconds) continue else: _call_handlers(on_success, **details, value=ret) @@ -82,6 +82,8 @@ def retry_exception(target, wait_gen, exception, *, max_tries, max_time, jitter, giveup, on_success, on_backoff, on_giveup, raise_on_giveup, + monotonic_time=None, + sleep=None, wait_gen_kwargs): @functools.wraps(target) @@ -93,11 +95,11 @@ def retry(*args, **kwargs): max_time = _maybe_call(max_time) tries = 0 - start = datetime.datetime.now() + start = (monotonic_time or time.monotonic)() wait = _init_wait_gen(wait_gen, wait_gen_kwargs) while True: tries += 1 - elapsed = timedelta.total_seconds(datetime.datetime.now() - start) + elapsed = (monotonic_time or time.monotonic)() - start details = { "target": target, "args": args, @@ -127,7 +129,7 @@ def retry(*args, **kwargs): _call_handlers(on_backoff, **details, wait=seconds) - time.sleep(seconds) + (sleep or time.sleep)(seconds) else: _call_handlers(on_success, **details) diff --git a/tests/test_backoff.py b/tests/test_backoff.py index e6b3657..f9110a8 100644 --- a/tests/test_backoff.py +++ b/tests/test_backoff.py @@ -1,5 +1,4 @@ # coding:utf-8 -import datetime import itertools import logging import random @@ -14,8 +13,26 @@ from tests.common import _save_target -def test_on_predicate(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +@pytest.fixture +def patch_time(monkeypatch): + now = 0.0 + log = [] + + def sleep(x): + nonlocal now + now += float(x) + log.append(x) + + def monotonic(): + return now + + monkeypatch.setattr('time.sleep', sleep) + monkeypatch.setattr('time.monotonic', monotonic) + + return log + + +def test_on_predicate(patch_time): @backoff.on_predicate(backoff.expo) def return_true(log, n): @@ -29,8 +46,7 @@ def return_true(log, n): assert 3 == len(log) -def test_on_predicate_max_tries(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_predicate_max_tries(patch_time): @backoff.on_predicate(backoff.expo, jitter=None, max_tries=3) def return_true(log, n): @@ -44,25 +60,13 @@ def return_true(log, n): assert 3 == len(log) -def test_on_predicate_max_time(monkeypatch): - nows = [ - datetime.datetime(2018, 1, 1, 12, 0, 10, 5), - datetime.datetime(2018, 1, 1, 12, 0, 9, 0), - datetime.datetime(2018, 1, 1, 12, 0, 1, 0), - datetime.datetime(2018, 1, 1, 12, 0, 0, 0), - ] - - class Datetime: - @staticmethod - def now(): - return nows.pop() - - monkeypatch.setattr('time.sleep', lambda x: None) - monkeypatch.setattr('datetime.datetime', Datetime) +def test_on_predicate_max_time(patch_time): def giveup(details): - assert details['tries'] == 3 - assert details['elapsed'] == 10.000005 + # Should be sleeps of 1, 2, 4 then 3 seconds, last one cut short. + # That's four sleeps in between five tries. + assert details['tries'] == 5 + assert details['elapsed'] == 10 @backoff.on_predicate(backoff.expo, jitter=None, max_time=10, on_giveup=giveup) @@ -74,11 +78,10 @@ def return_true(log, n): log = [] ret = return_true(log, 10) assert ret is False - assert len(log) == 3 + assert len(log) == 5 -def test_on_exception(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception(patch_time): @backoff.on_exception(backoff.expo, KeyError) def keyerror_then_true(log, n): @@ -93,8 +96,7 @@ def keyerror_then_true(log, n): assert 3 == len(log) -def test_on_exception_tuple(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_tuple(patch_time): @backoff.on_exception(backoff.expo, (KeyError, ValueError)) def keyerror_valueerror_then_true(log): @@ -114,8 +116,7 @@ def keyerror_valueerror_then_true(log): assert isinstance(log[1], ValueError) -def test_on_exception_max_tries(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_max_tries(patch_time): @backoff.on_exception(backoff.expo, KeyError, jitter=None, max_tries=3) def keyerror_then_true(log, n, foo=None): @@ -132,8 +133,7 @@ def keyerror_then_true(log, n, foo=None): assert 3 == len(log) -def test_on_exception_constant_iterable(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_constant_iterable(patch_time): backoffs = [] giveups = [] @@ -158,8 +158,7 @@ def endless_exceptions(): assert len(successes) == 0 -def test_on_exception_success_random_jitter(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_success_random_jitter(patch_time): backoffs, giveups, successes = [], [], [] @@ -188,8 +187,7 @@ def succeeder(*args, **kwargs): assert details['wait'] >= 0.5 * 2 ** i -def test_on_exception_success_full_jitter(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_success_full_jitter(patch_time): backoffs, giveups, successes = [], [], [] @@ -297,8 +295,7 @@ def exceptor(*args, **kwargs): 'tries': 3} -def test_on_exception_giveup_predicate(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_giveup_predicate(patch_time): def on_baz(e): return str(e) == "baz" @@ -432,8 +429,7 @@ def emptiness(*args, **kwargs): # To maintain backward compatibility, # on_predicate should support 0-argument jitter function. -def test_on_exception_success_0_arg_jitter(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_success_0_arg_jitter(patch_time, monkeypatch): monkeypatch.setattr('random.random', lambda: 0) backoffs, giveups, successes = [], [], [] @@ -480,8 +476,7 @@ def succeeder(*args, **kwargs): # To maintain backward compatibility, # on_predicate should support 0-argument jitter function. -def test_on_predicate_success_0_arg_jitter(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_predicate_success_0_arg_jitter(patch_time, monkeypatch): monkeypatch.setattr('random.random', lambda: 0) backoffs, giveups, successes = [], [], [] @@ -527,8 +522,7 @@ def success(*args, **kwargs): 'value': True} -def test_on_exception_callable_max_tries(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_callable_max_tries(patch_time, monkeypatch): def lookup_max_tries(): return 3 @@ -572,8 +566,7 @@ def exceptor(): exceptor() -def test_on_predicate_in_thread(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_predicate_in_thread(patch_time): result = [] @@ -603,8 +596,7 @@ def return_true(log, n): assert result[0] == 'success' -def test_on_predicate_constant_iterable(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_predicate_constant_iterable(patch_time): waits = [1, 2, 3, 6, 9] backoffs = [] @@ -632,8 +624,7 @@ def falsey(): assert len(successes) == 0 -def test_on_exception_in_thread(monkeypatch): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_in_thread(patch_time): result = [] @@ -664,8 +655,7 @@ def keyerror_then_true(log, n): assert result[0] == 'success' -def test_on_exception_logger_default(monkeypatch, caplog): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_logger_default(patch_time, caplog): logger = logging.getLogger('backoff') handler = logging.StreamHandler(sys.stdout) @@ -684,8 +674,7 @@ def key_error(): assert record.name == 'backoff' -def test_on_exception_logger_none(monkeypatch, caplog): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_logger_none(patch_time, caplog): logger = logging.getLogger('backoff') handler = logging.StreamHandler(sys.stdout) @@ -702,8 +691,7 @@ def key_error(): assert not caplog.records -def test_on_exception_logger_user(monkeypatch, caplog): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_logger_user(patch_time, caplog): logger = logging.getLogger('my-logger') handler = logging.StreamHandler(sys.stdout) @@ -722,8 +710,7 @@ def key_error(): assert record.name == 'my-logger' -def test_on_exception_logger_user_str(monkeypatch, caplog): - monkeypatch.setattr('time.sleep', lambda x: None) +def test_on_exception_logger_user_str(patch_time, caplog): logger = logging.getLogger('my-logger') handler = logging.StreamHandler(sys.stdout)