diff --git a/CHANGELOG.md b/CHANGELOG.md index 55bf2089a9914e9f22d9aa1d6a639355935f719c..2219cfd406024469c495ba63fd5a4331b5d930d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,3 @@ # v2.14.44 + +* added `ExponentialBackoff` diff --git a/docs/time.rst b/docs/time.rst index efec99eb24b1de87e440f59f0d352e0216523a1d..17613b9671635b431daf1e4f7722b766d9cbc4fa 100644 --- a/docs/time.rst +++ b/docs/time.rst @@ -35,3 +35,9 @@ sleep ----- .. autofunction:: satella.time.sleep + +ExponentialBackoff +------------------ + +.. autoclass:: satella.time.ExponentialBackoff + :members: diff --git a/satella/__init__.py b/satella/__init__.py index e1cb8af304bfca58d40e1da98aeb5e2b91a55a74..5f99ec5814825b96105db74b0fb7b0f1a50f8c76 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.14.44a1' +__version__ = '2.14.44' diff --git a/satella/time.py b/satella/time.py index 5fa1683f021bb400dec9472d31997ecf22dacab2..6df47795b088892b8bf6866427a85b042f1c4f3e 100644 --- a/satella/time.py +++ b/satella/time.py @@ -7,7 +7,7 @@ import warnings from concurrent.futures import Future from functools import wraps # import from functools to prevent circular import exception -__all__ = ['measure', 'time_as_int', 'time_ms', 'sleep', 'time_us'] +__all__ = ['measure', 'time_as_int', 'time_ms', 'sleep', 'time_us', 'ExponentialBackoff'] from satella.exceptions import WouldWaitMore @@ -321,3 +321,46 @@ class measure: if self.stop_on_stop: self.stop() return False + + +class ExponentialBackoff: + """ + A class that will sleep increasingly longer on errors. Meant to be used in such a way: + + >>> eb = ExponentialBackoff(start=2, limit=30) + >>> while not connect(): + >>> eb.failed() + >>> eb.sleep() + >>> eb.success() + + :param start: value at which to start + :param limit: maximum sleep timeout + :param sleep_fun: function used to sleep. Will accept a single argument - number of + seconds to wait + """ + __slots__ = ('start', 'limit', 'counter', 'sleep_fun') + + def __init__(self, start: float = 1, limit: float = 30, + sleep_fun: tp.Callable[[float], None] = sleep): + self.start = start + self.limit = limit + self.counter = start + self.sleep_fun = sleep_fun + + def sleep(self): + """ + Called when sleep is expected. + """ + self.sleep_fun(self.counter) + + def failed(self): + """ + Called when something fails. + """ + self.counter = min(self.limit, self.counter * 2) + + def success(self): + """ + Called when something successes. + """ + self.counter = self.start diff --git a/tests/test_time.py b/tests/test_time.py index cc35dcbb51856baa4915c97fbc305bdb66c9fe11..0699f8f0b90905e1c06de1ffce3d27fb9c9012ff 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -4,12 +4,21 @@ import time import multiprocessing import os import sys -from satella.time import measure, time_as_int, time_ms, sleep +from satella.time import measure, time_as_int, time_ms, sleep, ExponentialBackoff from concurrent.futures import Future class TestTime(unittest.TestCase): + def test_exponential_backoff(self): + with measure() as measurement: + eb = ExponentialBackoff() + eb.failed() + eb.sleep() + eb.failed() + eb.sleep() + self.assertGreaterEqual(measurement(), 2+4) + def test_measure(self): with measure(timeout=0.5) as measurement: self.assertFalse(measurement.timeouted)