Skip to content

Commit 3764e94

Browse files
committed
jenkinsapi.utils.retry: basic abstract retry handler
1 parent f0da26f commit 3764e94

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

jenkinsapi/utils/retry.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import time
2+
from abc import ABC, abstractmethod
3+
from dataclasses import dataclass
4+
5+
6+
class RetryConfig(ABC):
7+
"""
8+
Base class for retry configuration
9+
10+
Usage::
11+
12+
retry_check = retry_config.begin()
13+
while True:
14+
result = try_something()
15+
if result:
16+
return result
17+
retry_check.check_retry()
18+
19+
All state is stored in the `RetryCheck` instance so `RetryConfig` can be
20+
used in multiple contexts simultaneously.
21+
"""
22+
23+
@abstractmethod
24+
def begin(self) -> "RetryState": ...
25+
26+
27+
class RetryState(ABC):
28+
"""
29+
Base class for limited retry checks
30+
"""
31+
32+
@abstractmethod
33+
def check_retry(self) -> None:
34+
"""Sleep or raise `TimeoutError`"""
35+
pass
36+
37+
38+
@dataclass
39+
class SimpleRetryConfig(RetryConfig):
40+
sleep_period: float = 1
41+
timeout: float = 5
42+
43+
def begin(self) -> "SimpleRetryState":
44+
return SimpleRetryState(self)
45+
46+
47+
@dataclass
48+
class SimpleRetryState(RetryState):
49+
"""Basic implementation of RetryCheck with fixed sleep and timeout."""
50+
51+
config: SimpleRetryConfig
52+
start_time: float
53+
54+
def get_current_time(self) -> float:
55+
return time.monotonic()
56+
57+
def __init__(self, config: SimpleRetryConfig):
58+
self.config = config
59+
self.start_time = self.get_current_time()
60+
61+
def check_retry(self) -> None:
62+
curr_time = self.get_current_time()
63+
if curr_time - self.start_time > self.config.timeout:
64+
raise TimeoutError("Retry timed out")
65+
time.sleep(self.config.sleep_period)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from contextlib import ExitStack
2+
from unittest import mock
3+
4+
import pytest
5+
6+
from jenkinsapi.utils.retry import RetryConfig, RetryState, SimpleRetryConfig
7+
8+
9+
def validate_retry_check(
10+
retry_config: RetryConfig,
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_state = retry_config.begin()
26+
assert isinstance(retry_state, RetryState)
27+
while True:
28+
attempt_index += 1
29+
if attempt_index >= pass_index:
30+
success = True
31+
break
32+
retry_state.check_retry()
33+
if expected_pass:
34+
assert success
35+
else:
36+
assert success is False
37+
assert mock_sleep.call_count == expected_sleep_count
38+
39+
40+
def test_simple_retry_check():
41+
retry_config = SimpleRetryConfig(sleep_period=1, timeout=5)
42+
validate_retry_check(
43+
retry_config,
44+
pass_index=3,
45+
expected_sleep_count=2,
46+
expected_pass=True,
47+
)
48+
49+
50+
def test_simple_retry_check_fail():
51+
retry_config = SimpleRetryConfig(sleep_period=1, timeout=5)
52+
validate_retry_check(
53+
retry_config,
54+
pass_index=10,
55+
expected_sleep_count=5,
56+
expected_pass=False,
57+
)
58+
59+
60+
def test_repr():
61+
retry_config = SimpleRetryConfig(sleep_period=1, timeout=5)
62+
assert repr(retry_config) == "SimpleRetryConfig(sleep_period=1, timeout=5)"

0 commit comments

Comments
 (0)