| # 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() |