diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a8e916578e2e575fd3bfffa1ed90a42645a886..83a6a1721be36ba248af3f83b207f0d804fddb66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ * added automatic enum parsing to satella's JSONEncoder * Satella's JSON file handling will internally use Satella's JSONEncoder +* added ExponentialBackoff.launch diff --git a/satella/__init__.py b/satella/__init__.py index 606f71e73a30c299c13789b3a6ba7d8a59099505..49df3f053aaddc3886ae86c1df046209420dd996 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1 +1 @@ -__version__ = '2.17.17a3' +__version__ = '2.17.17a4' diff --git a/satella/time/backoff.py b/satella/time/backoff.py index a720d84ed883ebe7a9bbaff597cd4ba54ff11a26..68c3583b51c9b7c98027de83622a1aac254e56b1 100644 --- a/satella/time/backoff.py +++ b/satella/time/backoff.py @@ -1,6 +1,10 @@ import time import typing as tp +from satella.coding.decorators.decorators import wraps + +from satella.coding.typing import ExceptionList + from satella.coding.concurrent.thread import Condition from .measure import measure from ..exceptions import WouldWaitMore @@ -124,3 +128,40 @@ class ExponentialBackoff: self.grace_counter = 0 self.unavailable_until = None self.condition.notify_all() + + def launch(self, exceptions_on_failed: ExceptionList = Exception): + """ + A decorator to simplify writing doing-something loops. Basically, this: + + >>> eb = ExponentialBackoff(start=2.5, limit=30) + >>> @eb.launch(TypeError) + >>> def do_action(*args, **kwargs): + >>> x_do_action(*args, **kwargs) + >>> do_action(5, test=True) + + is equivalent to this: + + >>> eb = ExponentialBackoff(start=2.5, limit=30) + >>> while True: + >>> try: + >>> x_do_action(5, test=True) + >>> except TypeError: + >>> eb.failed() + >>> eb.sleep() + + :param exceptions_on_failed: a list of a single exception of exceptions + whose raising will signal that fun has failed + :return: a function, that called, will pass the exactly same parameters + """ + def outer(fun): + @wraps(fun) + def inner(*args, **kwargs): + try: + r = fun(*args, **kwargs) + self.success() + return r + except exceptions_on_failed: + self.failed() + self.sleep() + return inner + return outer diff --git a/tests/test_time.py b/tests/test_time.py index 41624ab1e7796a796144e1078f45e627ab3f0627..b33fb42f26a5ef0ac5795629c20f24cddc5d0896 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -38,6 +38,22 @@ class TestTime(unittest.TestCase): time.sleep(1) eb.success() + def test_exponential_backoff_launch(self): + eb = ExponentialBackoff(start=2, limit=30) + i = 1 + @eb.launch(ValueError) + def do_action(): + nonlocal i + if i == 3: + return + else: + i += 1 + raise ValueError() + + with measure() as m: + do_action() + self.assertGreaterEqual(m(), 2) + def test_exponential_backoff_waiting_for_service_healthy(self): eb = ExponentialBackoff(start=2, limit=30) self.assertTrue(eb.ready_for_next_check)