feat: add retry policy

This commit is contained in:
2026-02-20 02:44:32 +02:00
parent dec025b45a
commit b0f1828ae7
13 changed files with 303 additions and 68 deletions

View File

@@ -97,6 +97,26 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
},
},
},
"aur": {
"type": "dict",
"schema": {
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
},
},
"auth": {
"type": "dict",
"schema": {
@@ -296,10 +316,20 @@ CONFIGURATION_SCHEMA: ConfigurationSchema = {
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"password": {
"type": "string",
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"suppress_http_log_errors": {
"type": "boolean",
"coerce": "boolean",

View File

@@ -76,6 +76,19 @@ class Validator(RootValidator):
converted: bool = self.configuration._convert_to_boolean(value) # type: ignore[attr-defined]
return converted
def _normalize_coerce_float(self, value: str) -> float:
"""
extract float from string value
Args:
value(str): converting value
Returns:
float: value converted to float according to configuration rules
"""
del self
return float(value)
def _normalize_coerce_integer(self, value: str) -> int:
"""
extract integer from string value

View File

@@ -20,10 +20,9 @@
import contextlib
import requests
from functools import cached_property
from requests.adapters import BaseAdapter
from urllib.parse import urlparse
from ahriman import __version__
from ahriman.core.http.sync_http_client import SyncHttpClient
@@ -37,32 +36,36 @@ class SyncAhrimanClient(SyncHttpClient):
address: str
@cached_property
def session(self) -> requests.Session:
def _login_url(self) -> str:
"""
get or create session
get url for the login api
Returns:
request.Session: created session object
str: full url for web service to log in
"""
if urlparse(self.address).scheme == "http+unix":
import requests_unixsocket
session: requests.Session = requests_unixsocket.Session() # type: ignore[no-untyped-call]
session.headers["User-Agent"] = f"ahriman/{__version__}"
return session
return f"{self.address}/api/v1/login"
session = requests.Session()
session.headers["User-Agent"] = f"ahriman/{__version__}"
self._login(session)
return session
def _login(self, session: requests.Session) -> None:
def adapters(self) -> dict[str, BaseAdapter]:
"""
process login to the service
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
adapters = SyncHttpClient.adapters(self)
if (scheme := urlparse(self.address).scheme) == "http+unix":
from requests_unixsocket.adapters import UnixAdapter
adapters[f"{scheme}://"] = UnixAdapter() # type: ignore[no-untyped-call]
return adapters
def on_session_creation(self, session: requests.Session) -> None:
"""
method which will be called on session creation
Args:
session(requests.Session): request session to login
session(requests.Session): created requests session
"""
if self.auth is None:
return # no auth configured
@@ -74,12 +77,3 @@ class SyncAhrimanClient(SyncHttpClient):
}
with contextlib.suppress(Exception):
self.make_request("POST", self._login_url(), json=payload, session=session)
def _login_url(self) -> str:
"""
get url for the login api
Returns:
str: full url for web service to log in
"""
return f"{self.address}/api/v1/login"

View File

@@ -21,7 +21,9 @@ import requests
import sys
from functools import cached_property
from requests.adapters import BaseAdapter, HTTPAdapter
from typing import Any, IO, Literal
from urllib3.util.retry import Retry
from ahriman import __version__
from ahriman.core.configuration import Configuration
@@ -38,10 +40,14 @@ class SyncHttpClient(LazyLogging):
Attributes:
auth(tuple[str, str] | None): HTTP basic auth object if set
retry(Retry): retry policy of the HTTP client. Disabled by default
suppress_errors(bool): suppress logging of request errors
timeout(int | None): HTTP request timeout in seconds
"""
retry: Retry = Retry()
timeout: int | None = None
def __init__(self, configuration: Configuration | None = None, section: str | None = None, *,
suppress_errors: bool = False) -> None:
"""
@@ -50,18 +56,21 @@ class SyncHttpClient(LazyLogging):
section(str | None, optional): settings section name (Default value = None)
suppress_errors(bool, optional): suppress logging of request errors (Default value = False)
"""
if configuration is None:
configuration = Configuration() # dummy configuration
if section is None:
section = configuration.default_section
configuration = configuration or Configuration() # dummy configuration
section = section or configuration.default_section
username = configuration.get(section, "username", fallback=None)
password = configuration.get(section, "password", fallback=None)
self.auth = (username, password) if username and password else None
self.timeout: int | None = configuration.getint(section, "timeout", fallback=30)
self.suppress_errors = suppress_errors
self.timeout = configuration.getint(section, "timeout", fallback=30)
self.retry = SyncHttpClient.retry_policy(
max_retries=configuration.getint(section, "max_retries", fallback=0),
retry_backoff=configuration.getfloat(section, "retry_backoff", fallback=0.0),
)
@cached_property
def session(self) -> requests.Session:
"""
@@ -71,11 +80,17 @@ class SyncHttpClient(LazyLogging):
request.Session: created session object
"""
session = requests.Session()
for protocol, adapter in self.adapters().items():
session.mount(protocol, adapter)
python_version = ".".join(map(str, sys.version_info[:3])) # just major.minor.patch
session.headers["User-Agent"] = f"ahriman/{__version__} " \
f"{requests.utils.default_user_agent()} " \
f"python/{python_version}"
self.on_session_creation(session)
return session
@staticmethod
@@ -92,6 +107,39 @@ class SyncHttpClient(LazyLogging):
result: str = exception.response.text if exception.response is not None else ""
return result
@staticmethod
def retry_policy(max_retries: int = 0, retry_backoff: float = 0.0) -> Retry:
"""
build retry policy for class
Args:
max_retries(int, optional): maximum amount of retries allowed (Default value = 0)
retry_backoff(float, optional): retry exponential backoff (Default value = 0.0)
Returns:
Retry: built retry policy
"""
return Retry(
total=max_retries,
connect=max_retries,
read=max_retries,
status=max_retries,
status_forcelist=[429, 500, 502, 503, 504],
backoff_factor=retry_backoff,
)
def adapters(self) -> dict[str, BaseAdapter]:
"""
get registered adapters
Returns:
dict[str, BaseAdapter]: map of protocol and adapter used for this protocol
"""
return {
"http://": HTTPAdapter(max_retries=self.retry),
"https://": HTTPAdapter(max_retries=self.retry),
}
def make_request(self, method: Literal["DELETE", "GET", "HEAD", "POST", "PUT"], url: str, *,
headers: dict[str, str] | None = None,
params: list[tuple[str, str]] | None = None,
@@ -139,3 +187,11 @@ class SyncHttpClient(LazyLogging):
if not suppress_errors:
self.logger.exception("could not perform http request")
raise
def on_session_creation(self, session: requests.Session) -> None:
"""
method which will be called on session creation
Args:
session(requests.Session): created requests session
"""

View File

@@ -302,6 +302,16 @@ class ReportTrigger(Trigger):
"empty": False,
"is_url": [],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"rss_url": {
"type": "string",
"empty": False,

View File

@@ -21,6 +21,7 @@ from typing import Self
from ahriman.core import _Context, context
from ahriman.core.alpm.pacman import Pacman
from ahriman.core.alpm.remote import AUR
from ahriman.core.configuration import Configuration
from ahriman.core.database import SQLite
from ahriman.core.repository.executor import Executor
@@ -73,9 +74,26 @@ class Repository(Executor, UpdateHandler):
"""
instance = cls(repository_id, configuration, database,
report=report, refresh_pacman_database=refresh_pacman_database)
instance._set_globals(configuration)
instance._set_context()
return instance
@staticmethod
def _set_globals(configuration: Configuration) -> None:
"""
set global settings based on configuration via class attributes
Args:
configuration(Configuration): configuration instance
"""
AUR.timeout = configuration.getint("aur", "timeout", fallback=30)
AUR.retry = AUR.retry_policy(
max_retries=configuration.getint("aur", "max_retries", fallback=0),
retry_backoff=configuration.getfloat("aur", "retry_backoff", fallback=0.0),
)
def _set_context(self) -> None:
"""
create context variables and set their values

View File

@@ -54,6 +54,11 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["github"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"owner": {
"type": "string",
"required": True,
@@ -68,6 +73,11 @@ class UploadTrigger(Trigger):
"required": True,
"empty": False,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",
@@ -90,6 +100,16 @@ class UploadTrigger(Trigger):
"type": "string",
"allowed": ["ahriman", "remote-service"],
},
"max_retries": {
"type": "integer",
"coerce": "integer",
"min": 0,
},
"retry_backoff": {
"type": "float",
"coerce": "float",
"min": 0,
},
"timeout": {
"type": "integer",
"coerce": "integer",