mirror of
https://github.com/ansible-collections/hetzner.hcloud
synced 2024-11-10 06:34:13 +00:00
19e586fa22
##### SUMMARY Replace the constant poll interval of 1 second, with a truncated exponential back off algorithm with jitter. Below is a suite of poll interval (in seconds) generated by the new algorithm: ``` 1.49 2.14 5.46 6.51 6.57 5.57 5.98 7.13 6.59 7.10 5.54 5.03 6.56 5.96 6.72 7.21 7.05 5.31 5.60 6.33 6.82 5.42 6.08 6.60 TOTAL: 140.77 ```
128 lines
3.9 KiB
Python
128 lines
3.9 KiB
Python
# Copyright: (c) 2023, Hetzner Cloud GmbH <info@hetzner-cloud.de>
|
|
|
|
from __future__ import annotations
|
|
|
|
from contextlib import contextmanager
|
|
from random import random
|
|
|
|
from ansible.module_utils.basic import missing_required_lib
|
|
|
|
from .vendor.hcloud import APIException, Client as ClientBase
|
|
|
|
HAS_REQUESTS = True
|
|
HAS_DATEUTIL = True
|
|
|
|
try:
|
|
import requests # pylint: disable=unused-import
|
|
except ImportError:
|
|
HAS_REQUESTS = False
|
|
|
|
try:
|
|
import dateutil # pylint: disable=unused-import
|
|
except ImportError:
|
|
HAS_DATEUTIL = False
|
|
|
|
|
|
class ClientException(Exception):
|
|
"""An error related to the client occurred."""
|
|
|
|
|
|
def client_check_required_lib():
|
|
if not HAS_REQUESTS:
|
|
raise ClientException(missing_required_lib("requests"))
|
|
if not HAS_DATEUTIL:
|
|
raise ClientException(missing_required_lib("python-dateutil"))
|
|
|
|
|
|
def _client_resource_not_found(resource: str, param: str | int):
|
|
return ClientException(f"resource ({resource.rstrip('s')}) does not exist: {param}")
|
|
|
|
|
|
def client_get_by_name_or_id(client: Client, resource: str, param: str | int):
|
|
"""
|
|
Get a resource by name, and if not found by its ID.
|
|
|
|
:param client: Client to use to make the call
|
|
:param resource: Name of the resource client that implements both `get_by_name` and `get_by_id` methods
|
|
:param param: Name or ID of the resource to query
|
|
"""
|
|
resource_client = getattr(client, resource)
|
|
|
|
result = resource_client.get_by_name(param)
|
|
if result is not None:
|
|
return result
|
|
|
|
# If the param is not a valid ID, prevent an unnecessary call to the API.
|
|
try:
|
|
int(param)
|
|
except ValueError as exception:
|
|
raise _client_resource_not_found(resource, param) from exception
|
|
|
|
try:
|
|
return resource_client.get_by_id(param)
|
|
except APIException as exception:
|
|
if exception.code == "not_found":
|
|
raise _client_resource_not_found(resource, param) from exception
|
|
raise exception
|
|
|
|
|
|
if HAS_REQUESTS:
|
|
|
|
class CachedSession(requests.Session):
|
|
cache: dict[str, requests.Response]
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.cache = {}
|
|
|
|
def send(self, request: requests.PreparedRequest, **kwargs) -> requests.Response: # type: ignore[no-untyped-def]
|
|
"""
|
|
Send a given PreparedRequest.
|
|
"""
|
|
if request.method != "GET" or request.url is None:
|
|
return super().send(request, **kwargs)
|
|
|
|
if request.url in self.cache:
|
|
return self.cache[request.url]
|
|
|
|
response = super().send(request, **kwargs)
|
|
if response.ok:
|
|
self.cache[request.url] = response
|
|
|
|
return response
|
|
|
|
|
|
class Client(ClientBase):
|
|
@contextmanager
|
|
def cached_session(self):
|
|
"""
|
|
Swap the client session during the scope of the context. The session will cache
|
|
all GET requests.
|
|
|
|
Cached response will not expire, therefore the cached client must not be used
|
|
for long living scopes.
|
|
"""
|
|
self._requests_session = CachedSession()
|
|
try:
|
|
yield
|
|
finally:
|
|
self._requests_session = requests.Session()
|
|
|
|
|
|
def exponential_backoff_poll_interval(*, base: float, multiplier: int, cap: float, jitter: float):
|
|
"""
|
|
Return a poll interval function, implementing a truncated exponential backoff with jitter.
|
|
|
|
:param base: Base for the exponential backoff algorithm.
|
|
:param multiplier: Multiplier for the exponential backoff algorithm.
|
|
:param cap: Value at which the interval is truncated.
|
|
:param jitter: Proportion of the interval to add as random jitter.
|
|
"""
|
|
|
|
def func(retries: int) -> float:
|
|
interval = base * multiplier**retries # Exponential backoff
|
|
interval = min(cap, interval) # Cap backoff
|
|
interval += random() * interval * jitter # Add jitter
|
|
return interval
|
|
|
|
return func
|