From 5a367dc1f7e4746ab2f00b884bfd13c40f54b6c1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Sun, 15 Aug 2021 14:20:17 +0200
Subject: [PATCH] add ExponentialBackoff.launch

---
 CHANGELOG.md            |  1 +
 satella/__init__.py     |  2 +-
 satella/time/backoff.py | 41 +++++++++++++++++++++++++++++++++++++++++
 tests/test_time.py      | 16 ++++++++++++++++
 4 files changed, 59 insertions(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 10a8e916..83a6a172 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 606f71e7..49df3f05 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 a720d84e..68c3583b 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 41624ab1..b33fb42f 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)
-- 
GitLab