feat: add retry logic for transient failures
Build and Push API / release (push) Successful in 3s
Build and Push API / build (push) Successful in 1m14s

- @with_retry decorator and retry_async() function
- Exponential backoff with jitter
- Retries on: timeout, connection errors, HTTP 429/5xx
- Web search tool now retries on network failures
- Configurable via RETRY_MAX_ATTEMPTS, RETRY_BASE_DELAY, RETRY_MAX_DELAY
- 29 new tests (205 total passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 23:22:16 +01:00
co-authored by Claude Opus 4.5
parent 2f97041aa9
commit acf231eb66
8 changed files with 509 additions and 12 deletions
+27 -9
View File
@@ -9,6 +9,7 @@ import httpx
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger
from src.shared.retry import retry_async
logger = get_logger(__name__)
@@ -73,6 +74,24 @@ IMPORTANT:
self.searxng_url = (searxng_url or settings.searxng_url).rstrip("/")
self.timeout = timeout or settings.searxng_timeout
self.max_results = max_results
# Retry settings
self.retry_max_attempts = settings.retry_max_attempts
self.retry_base_delay = settings.retry_base_delay
self.retry_max_delay = settings.retry_max_delay
async def _fetch_search_results(self, params: dict) -> dict:
"""
Fetch search results from SearXNG.
This method is wrapped with retry logic for transient failures.
"""
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
f"{self.searxng_url}/search",
params=params,
)
response.raise_for_status()
return response.json()
@logged()
async def execute(
@@ -111,16 +130,15 @@ IMPORTANT:
params["categories"] = categories
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.get(
f"{self.searxng_url}/search",
params=params,
)
response.raise_for_status()
data = response.json()
data = await retry_async(
self._fetch_search_results,
params,
max_attempts=self.retry_max_attempts,
base_delay=self.retry_base_delay,
max_delay=self.retry_max_delay,
)
except httpx.TimeoutException:
return self._error(f"Search timed out after {self.timeout}s")
return self._error(f"Search timed out after {self.timeout}s (all retries exhausted)")
except httpx.HTTPStatusError as e:
return self._error(f"Search failed: HTTP {e.response.status_code}")
except httpx.RequestError as e: