Skip to content

Commit 7fe6c25

Browse files
committed
jenkinsapi.utils.retry: basic abstract retry handler
1 parent dfb7630 commit 7fe6c25

File tree

2 files changed

+120
-0
lines changed

2 files changed

+120
-0
lines changed

jenkinsapi/utils/retry.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from abc import ABC, abstractmethod
2+
import time
3+
from typing import Optional
4+
5+
6+
class Retry(ABC):
7+
"""
8+
Base class for limited retry checks
9+
10+
Usage::
11+
12+
retry.begin()
13+
while True:
14+
result = try_something()
15+
if result:
16+
return result
17+
retry.check()
18+
"""
19+
20+
def begin(self) -> None:
21+
pass
22+
23+
@abstractmethod
24+
def check(self) -> None:
25+
"""Sleep or raise `TimeoutError`"""
26+
27+
28+
class SimpleRetry(Retry):
29+
"""Basic implementation of RetryCheck with fixed sleep and timeout."""
30+
31+
def get_current_time(self) -> float:
32+
return time.monotonic()
33+
34+
start_time: Optional[float] = None
35+
36+
def __init__(self, sleep_period: float = 1, timeout: float = 5):
37+
self.sleep_period = sleep_period
38+
self.timeout = timeout
39+
40+
def begin(self) -> None:
41+
self.start_time = self.get_current_time()
42+
43+
def check(self) -> None:
44+
start_time = self.start_time
45+
if start_time is None:
46+
raise RuntimeError(
47+
"Retry has not been started. Call begin() first."
48+
)
49+
curr_time = self.get_current_time()
50+
if curr_time - start_time > self.timeout:
51+
raise TimeoutError("Retry timed out")
52+
time.sleep(self.sleep_period)
53+
54+
def __repr__(self) -> str:
55+
return (
56+
f"{self.__class__.__name__}("
57+
f"sleep_period={self.sleep_period}, "
58+
f"timeout={self.timeout})"
59+
)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from contextlib import ExitStack
2+
from unittest import mock
3+
4+
import pytest
5+
6+
from jenkinsapi.utils.retry import Retry, SimpleRetry
7+
8+
9+
def validate_retry_check(
10+
retry: Retry,
11+
pass_index: int,
12+
expected_sleep_count: int,
13+
expected_pass: bool = True,
14+
) -> None:
15+
"""Check if the retry check works as expected."""
16+
attempt_index = 0
17+
success = False
18+
with ExitStack() as exit_stack:
19+
exit_stack.enter_context(
20+
mock.patch("time.monotonic", side_effect=range(100, 1000))
21+
)
22+
mock_sleep = exit_stack.enter_context(mock.patch("time.sleep"))
23+
if not expected_pass:
24+
exit_stack.enter_context(pytest.raises(TimeoutError))
25+
retry.begin()
26+
while True:
27+
attempt_index += 1
28+
if attempt_index >= pass_index:
29+
success = True
30+
break
31+
retry.check()
32+
if expected_pass:
33+
assert success
34+
else:
35+
assert success is False
36+
assert mock_sleep.call_count == expected_sleep_count
37+
38+
39+
def test_simple_retry_check():
40+
retry = SimpleRetry(sleep_period=1, timeout=5)
41+
validate_retry_check(
42+
retry,
43+
pass_index=3,
44+
expected_sleep_count=2,
45+
expected_pass=True,
46+
)
47+
48+
49+
def test_simple_retry_check_fail():
50+
retry = SimpleRetry(sleep_period=1, timeout=5)
51+
validate_retry_check(
52+
retry,
53+
pass_index=10,
54+
expected_sleep_count=5,
55+
expected_pass=False,
56+
)
57+
58+
59+
def test_repr():
60+
retry_check = SimpleRetry(sleep_period=1, timeout=5)
61+
assert repr(retry_check) == "SimpleRetry(sleep_period=1, timeout=5)"

0 commit comments

Comments
 (0)