From ac974739a53b6e3fc7a0b25d5d1c01cba7c2f6e2 Mon Sep 17 00:00:00 2001
From: Lukas Slebodnik <lslebodn@redhat.com>
Date: Tue, 5 Feb 2019 10:42:08 +0100
Subject: [PATCH] Utils: Add decorator for retry
It might be useful in checks which do lot of network communication.
---
colin/utils/cmd_tools.py | 29 +++++++++++
tests/unit/test_utils.py | 103 +++++++++++++++++++++++++++++++++++++--
2 files changed, 129 insertions(+), 3 deletions(-)
diff --git a/colin/utils/cmd_tools.py b/colin/utils/cmd_tools.py
index 2da4118..f3f08a8 100644
--- a/colin/utils/cmd_tools.py
+++ b/colin/utils/cmd_tools.py
@@ -13,9 +13,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
+import functools
import logging
import subprocess
import threading
+import time
try:
import thread
@@ -120,3 +122,30 @@ def inner(*args, **kwargs):
return inner
return outer
+
+
+def retry(retry_count=5, delay=2):
+ """
+ Use as decorator to retry functions few times with delays
+
+ Exception will be raised if last call fails
+
+ :param retry_count: int could of retries in case of failures. It must be
+ a positive number
+ :param delay: int delay between retries
+ """
+ if retry_count <= 0:
+ raise ValueError("retry_count have to be positive")
+
+ def decorator(f):
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ for i in range(retry_count, 0, -1):
+ try:
+ return f(*args, **kwargs)
+ except Exception:
+ if i <= 1:
+ raise
+ time.sleep(delay)
+ return wrapper
+ return decorator
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 0d23182..4185a56 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -13,12 +13,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
-from time import sleep
+import time
import pytest
from colin.core.exceptions import ColinException
from colin.utils.cmd_tools import exit_after
+from colin.utils.cmd_tools import retry
@exit_after(1)
@@ -28,7 +29,7 @@ def fast_fce():
@exit_after(1)
def slow_fce():
- sleep(2)
+ time.sleep(2)
@exit_after(1)
@@ -52,4 +53,100 @@ def test_timeout_bad_fce():
def test_timeout_dirrect():
with pytest.raises(TimeoutError):
- exit_after(1)(sleep)(2)
+ exit_after(1)(time.sleep)(2)
+
+
+COUNTER = 0
+
+
+def raise_exception():
+ global COUNTER
+ COUNTER += 1
+
+ raise Exception('I am bad function')
+
+
+def test_no_retry_for_success():
+ global COUNTER
+ COUNTER = 0
+
+ @retry(5, 0)
+ def always_success():
+ global COUNTER
+ COUNTER = COUNTER + 1
+
+ return 42
+
+ assert always_success() == 42
+ assert COUNTER == 1
+
+
+def test_retry_with_exception():
+ global COUNTER
+ COUNTER = 0
+
+ @retry(5, 0)
+ def always_raise_exception():
+ raise_exception()
+
+ with pytest.raises(Exception) as ex:
+ always_raise_exception()
+
+ assert str(ex.value) == 'I am bad function'
+ assert COUNTER == 5
+
+
+def test_wrong_parameter():
+ with pytest.raises(ValueError) as ex:
+ retry(-1, 1)
+ assert str(ex.value) == 'retry_count have to be positive'
+
+ with pytest.raises(ValueError) as ex:
+ retry(0, 1)
+ assert str(ex.value) == 'retry_count have to be positive'
+
+ @retry(5, -1)
+ def fail_negative_sleep():
+ raise_exception()
+
+ with pytest.raises(ValueError) as ex:
+ fail_negative_sleep()
+ assert str(ex.value) == 'sleep length must be non-negative'
+
+
+def test_retry_with_sleep():
+ global COUNTER
+ COUNTER = 0
+
+ @retry(4, .5)
+ def fail_and_sleep():
+ raise_exception()
+
+ time_start = time.time()
+ with pytest.raises(Exception) as ex:
+ fail_and_sleep()
+ time_end = time.time()
+
+ assert str(ex.value) == 'I am bad function'
+ assert COUNTER == 4
+
+ # there are 3 sleeps between 4 delays
+ assert time_end - time_start >= 1.5
+ # there were not 4 sleeps
+ assert time_end - time_start < 4
+
+
+def test_recover_after_few_failures():
+ global COUNTER
+ COUNTER = 0
+
+ @retry(5, 0)
+ def sleep_like_a_baby():
+ global COUNTER
+ if COUNTER < 3:
+ COUNTER += 1
+ raise Exception("sleeping")
+ return []
+
+ assert sleep_like_a_baby() == []
+ assert COUNTER == 3