| |
@@ -0,0 +1,188 @@
|
| |
+ 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
|
| |