From a942fdffc1ec405cbf6347da58be24c2e8202b50 Mon Sep 17 00:00:00 2001 From: Paul Codding Date: Sat, 20 Sep 2025 10:36:27 -0500 Subject: [PATCH] Added exponential backoff capabilities We're seeing a number of connection issues with the Serper API that resolve themselves when retrying. Instead of requiring the agent to do this and handling it in the Runbook, we should handle it in the action. --- actions/serper/CHANGELOG.md | 6 ++++ actions/serper/README.md | 1 + actions/serper/actions.py | 61 +++++++++++++++++++++++++++++++++++-- actions/serper/package.yaml | 9 +++--- 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/actions/serper/CHANGELOG.md b/actions/serper/CHANGELOG.md index 8a473c37..396bc019 100644 --- a/actions/serper/CHANGELOG.md +++ b/actions/serper/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/) and this project adheres to [Semantic Versioning](https://semver.org/). +## [1.1.3] - 2025-01-27 + +### Added + +- Exponential backoff retry logic for handling connection failures (RemoteDisconnected exceptions) + ## [1.1.2] - 2025-08-07 ### Changed diff --git a/actions/serper/README.md b/actions/serper/README.md index c93311e9..72945e0a 100644 --- a/actions/serper/README.md +++ b/actions/serper/README.md @@ -7,6 +7,7 @@ The Serper Action Package provides an interface to interact with the Serper API, - Perform Google searches using the Serper API. - Retrieve structured search results, including knowledge graph, organic results, places, people also ask, and related searches. - Securely manage API keys using Sema4.ai's Secret management. +- Built-in retry logic with exponential backoff for handling connection failures. ## Installation diff --git a/actions/serper/actions.py b/actions/serper/actions.py index 28624e96..5b7f6489 100644 --- a/actions/serper/actions.py +++ b/actions/serper/actions.py @@ -1,5 +1,6 @@ import json import os +import time from pathlib import Path from typing import List, Optional @@ -11,6 +12,58 @@ load_dotenv(Path(__file__).absolute().parent / "devdata" / ".env") +def retry_with_exponential_backoff(func, max_retries=3, base_delay=1.0, max_delay=5.0): + """ + Retry a function with exponential backoff for connection-related exceptions. + + Args: + func: The function to retry + max_retries: Maximum number of retry attempts (default: 3) + base_delay: Base delay in seconds for exponential backoff (default: 1.0) + max_delay: Maximum delay in seconds (default: 5.0) + + Returns: + The result of the function call + + Raises: + ActionError: If all retries are exhausted + """ + last_exception = None + + for attempt in range(max_retries + 1): + try: + return func() + except (HTTPError, Exception) as e: + last_exception = e + + # Check if it's a connection-related error that should be retried + error_str = str(e).lower() + is_connection_error = ( + "remote end closed connection" in error_str or + "connection aborted" in error_str or + "remotedisconnected" in error_str or + "connection reset" in error_str or + "broken pipe" in error_str + ) + + if not is_connection_error and not isinstance(e, HTTPError): + # Not a retryable error, raise immediately + raise ActionError(f"Non-retryable error occurred: {str(e)}") + + if attempt == max_retries: + # Last attempt failed, raise the exception + if is_connection_error: + raise ActionError(f"Remote connection failed after {max_retries} retries: {str(e)}") + else: + raise ActionError(f"HTTP error occurred: {str(e)}") + + # Calculate delay with exponential backoff + delay = min(base_delay * (2 ** attempt), max_delay) + time.sleep(delay) + + # This should never be reached, but just in case + raise ActionError(f"Unexpected error: {str(last_exception)}") + # Define Pydantic models for the response class KnowledgeGraph(BaseModel): @@ -81,7 +134,7 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: if not api_key: raise ActionError("API key is required but not provided") - try: + def make_request(): headers = {"X-API-KEY": api_key, "Content-Type": "application/json"} payload = json.dumps({"q": q, "num": num}) @@ -92,10 +145,12 @@ def search_google(q: str, num: int, api_key: Secret) -> Response[SearchResult]: ) response.raise_for_status() + return response + try: + # Use retry logic with exponential backoff + response = retry_with_exponential_backoff(make_request) search_result = SearchResult(**response.json()) return Response(result=search_result) - except HTTPError as e: - raise ActionError(f"HTTP error occurred: {str(e)}") except Exception as e: raise ActionError(f"An unexpected error occurred: {str(e)}") diff --git a/actions/serper/package.yaml b/actions/serper/package.yaml index ecc1607d..db5ff6ce 100644 --- a/actions/serper/package.yaml +++ b/actions/serper/package.yaml @@ -5,7 +5,7 @@ name: Serper description: Interact with the Serper API to perform Google searches. # Required: The version of the action package. -version: 1.1.2 +version: 1.1.3 # The version of the `package.yaml` format. spec-version: v2 @@ -13,11 +13,12 @@ spec-version: v2 dependencies: conda-forge: - python=3.11.11 - - python-dotenv=1.1.0 + - python-dotenv=1.1.1 - uv=0.6.11 pypi: - - sema4ai-actions=1.4.1 - - pydantic=2.11.7 + - sema4ai-actions=1.4.2 + - pydantic=2.11.9 + - urllib3=2.5.0 external-endpoints: - name: "Google Serper API"