From cd926d6909ff3992bcec127058b913a304c52c13 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Tue, 23 Feb 2021 16:52:52 +0100
Subject: [PATCH] add ExponentialBackoff

---
 CHANGELOG.md        |  2 ++
 docs/time.rst       |  6 ++++++
 satella/__init__.py |  2 +-
 satella/time.py     | 45 ++++++++++++++++++++++++++++++++++++++++++++-
 tests/test_time.py  | 11 ++++++++++-
 5 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55bf2089..2219cfd4 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 efec99eb..17613b96 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 e1cb8af3..5f99ec58 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 5fa1683f..6df47795 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 cc35dcbb..0699f8f0 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)
-- 
GitLab