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