Skip to content

Commit 98cfa14

Browse files
committed
Use higher precision time source in retry decorator/tests
Despite my best efforts to account for Windows' coarse timer precision, the test_retry_wait tests are still randomly failing as it isn't sleeping for long enough by a small margin: Example 1: 472.171 - 472.156 >= 0.015 (~1% too low) Example 2: 554.406 - 554.265 >= 0.15 (~7% too low) (call_time, test_start_time >= expected sleep duration between retries) To avoid these random failures, I've adjusted the required sleep duration to be 10% less which should be enough. I've also replaced time.monotonic() with time.perf_counter() in the retry utility and test suite since it's effectively monotonic[^1] while providing much higher resolution (esp. on Windows). [^1]: It's not guaranteed to be monotonic in the Python docs, but it is _indeed_ monotonic on all platforms we care abouti.
1 parent 6b54c19 commit 98cfa14

File tree

2 files changed

+14
-10
lines changed

2 files changed

+14
-10
lines changed

src/pip/_internal/utils/retry.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import functools
2-
from time import monotonic, sleep
2+
from time import perf_counter, sleep
33
from typing import Callable, TypeVar
44

55
from pip._vendor.typing_extensions import ParamSpec
@@ -26,12 +26,14 @@ def wrapper(func: Callable[P, T]) -> Callable[P, T]:
2626

2727
@functools.wraps(func)
2828
def retry_wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
29-
start_time = monotonic()
29+
# The performance counter is monotonic on all platforms we care
30+
# about and has much better resolution than time.monotonic().
31+
start_time = perf_counter()
3032
while True:
3133
try:
3234
return func(*args, **kwargs)
3335
except Exception:
34-
if monotonic() - start_time > stop_after_delay:
36+
if perf_counter() - start_time > stop_after_delay:
3537
raise
3638
sleep(wait)
3739

tests/unit/test_utils_retry.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import random
2-
from time import monotonic, sleep
2+
from time import perf_counter, sleep
33
from typing import List, NoReturn, Tuple, Type
44
from unittest.mock import Mock
55

@@ -65,7 +65,7 @@ def create_timestamped_callable(sleep_per_call: float = 0) -> Tuple[Mock, List[f
6565
timestamps = []
6666

6767
def _raise_error() -> NoReturn:
68-
timestamps.append(monotonic())
68+
timestamps.append(perf_counter())
6969
if sleep_per_call:
7070
sleep(sleep_per_call)
7171
raise RuntimeError
@@ -79,25 +79,27 @@ def test_retry_wait(wait_duration: float) -> None:
7979
function, timestamps = create_timestamped_callable()
8080
# Only the first retry will be scheduled before the time limit is exceeded.
8181
wrapped = retry(wait=wait_duration, stop_after_delay=0.01)(function)
82-
start_time = monotonic()
82+
start_time = perf_counter()
8383
with pytest.raises(RuntimeError):
8484
wrapped()
8585
assert len(timestamps) == 2
86-
assert timestamps[1] - start_time >= wait_duration
86+
# Add a margin of 10% to permit for unavoidable variation.
87+
assert timestamps[1] - start_time >= (wait_duration * 0.9)
8788

8889

8990
@pytest.mark.parametrize(
90-
"call_duration, max_allowed_calls", [(0.01, 10), (0.04, 3), (0.15, 1)]
91+
"call_duration, max_allowed_calls", [(0.01, 11), (0.04, 3), (0.15, 1)]
9192
)
9293
def test_retry_time_limit(call_duration: float, max_allowed_calls: int) -> None:
9394
function, timestamps = create_timestamped_callable(sleep_per_call=call_duration)
9495
wrapped = retry(wait=0, stop_after_delay=0.1)(function)
9596

96-
start_time = monotonic()
97+
start_time = perf_counter()
9798
with pytest.raises(RuntimeError):
9899
wrapped()
99100
assert len(timestamps) <= max_allowed_calls
100-
assert all(t - start_time <= 0.1 for t in timestamps)
101+
# Add a margin of 10% to permit for unavoidable variation.
102+
assert all(t - start_time <= (0.1 * 1.1) for t in timestamps)
101103

102104

103105
def test_retry_method() -> None:

0 commit comments

Comments
 (0)