blob: ad70c50b7bc647fb6b33adf6d786ba9ed6a12fc0 [file] [log] [blame]
# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import annotations
import datetime as dt
import logging
import math
import time
from typing import TYPE_CHECKING, Iterator, Optional
if TYPE_CHECKING:
from crossbench.runner.timing import AnyTime, AnyTimeUnit
def as_timedelta(value: float | dt.timedelta) -> dt.timedelta:
if isinstance(value, dt.timedelta):
return value
return dt.timedelta(seconds=value)
class WaitRange:
"""
Create wait/sleep ranges with the given parameters:
If present we start with the initial delay, and then exponentially
increase the sleep/wait time by the given factor, until we reach the max
sleep time.
| delay | min | min * factor | ... | min * factor ** N | ... | max |
| --------------------------- timeout ------------------------------|
| i=0 | i=1 | i=2 | ............... | i=max_iterations-1 |
The timeout puts an upper bound to the total sleep time when using
wait_with_backoff().
"""
def __init__(self,
min: AnyTime = 0.1,
timeout: AnyTime = 10,
factor: float = 1.01,
max: Optional[AnyTime] = None,
max_iterations: float = math.inf,
delay: AnyTime = 0) -> None:
self._min: dt.timedelta = as_timedelta(min)
assert self._min.total_seconds() > 0
if not max:
self._max: dt.timedelta = self._min * 10
else:
self._max = as_timedelta(max)
assert self._min <= self._max
assert 1.0 < factor
self._factor: float = factor
self._timeout: dt.timedelta = as_timedelta(timeout)
assert 0 < self._timeout.total_seconds()
self._delay = as_timedelta(delay)
assert self._delay <= self._timeout
assert max_iterations > 0
self._max_iterations: int | float = max_iterations
@property
def timeout(self) -> dt.timedelta:
return self._timeout
def __iter__(self) -> Iterator[tuple[int, dt.timedelta]]:
i = 0
if self._delay:
yield i, self._delay
i += 1
current_sleep = self._min
while True:
if self._max_iterations <= i:
break
yield i, current_sleep
current_sleep = min(current_sleep * self._factor, self._max)
i += 1
def wait_with_backoff(
self,) -> Iterator[tuple[int, dt.timedelta, dt.timedelta]]:
start = dt.datetime.now()
timeout = self._timeout
for i, sleep_for in self:
duration = dt.datetime.now() - start
if duration > self._timeout:
raise TimeoutError(f"Waited for {duration}")
time_left = timeout - duration
yield i, duration, time_left
sleep_f(sleep_for.total_seconds())
def sleep(seconds: AnyTimeUnit) -> None:
if isinstance(seconds, dt.timedelta):
seconds = seconds.total_seconds()
sleep_f(seconds)
def sleep_f(seconds: float) -> None:
if seconds == 0:
return
logging.debug("WAIT %ss", seconds)
time.sleep(seconds)
def wait_with_backoff(
wait_range: AnyTime | WaitRange,
) -> Iterator[tuple[int, dt.timedelta, dt.timedelta]]:
if not isinstance(wait_range, WaitRange):
wait_range = WaitRange(timeout=wait_range)
return wait_range.wait_with_backoff()