|
22 | 22 | import json |
23 | 23 | import logging |
24 | 24 | import os |
| 25 | +import random |
25 | 26 | import threading |
26 | 27 | import time |
27 | 28 | from typing import Any, Callable, Literal, Optional, Union, cast |
28 | 29 | import uuid |
29 | 30 |
|
30 | 31 | from google.api_core import exceptions as api_exceptions |
31 | 32 | import vertexai |
| 33 | +from google.genai import errors as genai_errors |
32 | 34 | from google.genai import types as genai_types |
33 | 35 | from google.genai._api_client import BaseApiClient |
34 | 36 | from google.genai.models import Models |
@@ -129,6 +131,45 @@ def _get_agent_engine_instance( |
129 | 131 | return _thread_local_data.agent_engine_instances[agent_name] |
130 | 132 |
|
131 | 133 |
|
| 134 | +def _is_retryable_error(e: Exception) -> bool: |
| 135 | + """Returns True for transient errors that are safe to retry with backoff.""" |
| 136 | + if isinstance(e, genai_errors.APIError): |
| 137 | + return e.code in _evals_constant.RETRYABLE_STATUS_CODES |
| 138 | + return isinstance( |
| 139 | + e, (api_exceptions.ResourceExhausted, api_exceptions.ServiceUnavailable) |
| 140 | + ) |
| 141 | + |
| 142 | + |
| 143 | +def _get_retry_backoff( |
| 144 | + e: Exception, attempt: int, max_retries: int, operation: str |
| 145 | +) -> Optional[float]: |
| 146 | + """Returns the seconds to wait before the next attempt, or None to stop. |
| 147 | +
|
| 148 | + Retryable errors back off exponentially with jitter. Other errors fail |
| 149 | + fast, and the last attempt never waits. |
| 150 | + """ |
| 151 | + if not _is_retryable_error(e) or attempt == max_retries - 1: |
| 152 | + logger.error( |
| 153 | + "Error during %s on attempt %d/%d: %s", |
| 154 | + operation, |
| 155 | + attempt + 1, |
| 156 | + max_retries, |
| 157 | + e, |
| 158 | + ) |
| 159 | + return None |
| 160 | + backoff = 2.0**attempt + random.uniform(0, 1) |
| 161 | + logger.warning( |
| 162 | + "Retryable error during %s on attempt %d/%d: %s. Retrying in %.1f" |
| 163 | + " seconds...", |
| 164 | + operation, |
| 165 | + attempt + 1, |
| 166 | + max_retries, |
| 167 | + e, |
| 168 | + backoff, |
| 169 | + ) |
| 170 | + return backoff |
| 171 | + |
| 172 | + |
132 | 173 | def _generate_content_with_retry( |
133 | 174 | api_client: BaseApiClient, |
134 | 175 | model: str, |
@@ -191,29 +232,11 @@ def _generate_content_with_retry( |
191 | 232 | } |
192 | 233 | else: |
193 | 234 | return response |
194 | | - except api_exceptions.ResourceExhausted as e: |
195 | | - logger.warning( |
196 | | - "Resource Exhausted error on attempt %d/%d: %s. Retrying in %s" |
197 | | - " seconds...", |
198 | | - attempt + 1, |
199 | | - max_retries, |
200 | | - e, |
201 | | - 2**attempt, |
202 | | - ) |
203 | | - if attempt == max_retries - 1: |
204 | | - return {"error": f"Resource exhausted after retries: {e}"} |
205 | | - time.sleep(2**attempt) |
206 | 235 | except Exception as e: # pylint: disable=broad-exception-caught |
207 | | - logger.error( |
208 | | - "Unexpected error during generate_content on attempt %d/%d: %s", |
209 | | - attempt + 1, |
210 | | - max_retries, |
211 | | - e, |
212 | | - ) |
213 | | - |
214 | | - if attempt == max_retries - 1: |
215 | | - return {"error": f"Failed after retries: {e}"} |
216 | | - time.sleep(1) |
| 236 | + backoff = _get_retry_backoff(e, attempt, max_retries, "generate_content") |
| 237 | + if backoff is None: |
| 238 | + return {"error": f"Failed on attempt {attempt + 1}/{max_retries}: {e}"} |
| 239 | + time.sleep(backoff) |
217 | 240 | return {"error": f"Failed to generate content after {max_retries} retries"} |
218 | 241 |
|
219 | 242 |
|
@@ -2352,28 +2375,11 @@ def _execute_agent_run_with_retry( |
2352 | 2375 | if event and CONTENT in event and PARTS in event[CONTENT]: |
2353 | 2376 | responses.append(event) |
2354 | 2377 | return responses |
2355 | | - except api_exceptions.ResourceExhausted as e: |
2356 | | - logger.warning( |
2357 | | - "Resource Exhausted error on attempt %d/%d: %s. Retrying in %s" |
2358 | | - " seconds...", |
2359 | | - attempt + 1, |
2360 | | - max_retries, |
2361 | | - e, |
2362 | | - 2**attempt, |
2363 | | - ) |
2364 | | - if attempt == max_retries - 1: |
2365 | | - return {"error": f"Resource exhausted after retries: {e}"} |
2366 | | - time.sleep(2**attempt) |
2367 | 2378 | except Exception as e: # pylint: disable=broad-exception-caught |
2368 | | - logger.error( |
2369 | | - "Unexpected error during agent engine run on attempt %d/%d: %s", |
2370 | | - attempt + 1, |
2371 | | - max_retries, |
2372 | | - e, |
2373 | | - ) |
2374 | | - if attempt == max_retries - 1: |
2375 | | - return {"error": f"Failed after retries: {e}"} |
2376 | | - time.sleep(1) |
| 2379 | + backoff = _get_retry_backoff(e, attempt, max_retries, "agent engine run") |
| 2380 | + if backoff is None: |
| 2381 | + return {"error": f"Failed on attempt {attempt + 1}/{max_retries}: {e}"} |
| 2382 | + time.sleep(backoff) |
2377 | 2383 | return {"error": f"Failed to get agent run results after {max_retries} retries"} |
2378 | 2384 |
|
2379 | 2385 |
|
@@ -2475,28 +2481,13 @@ async def _execute_local_agent_run_with_retry_async( |
2475 | 2481 | if event and CONTENT in event and PARTS in event[CONTENT]: |
2476 | 2482 | events.append(event) |
2477 | 2483 | return events |
2478 | | - except api_exceptions.ResourceExhausted as e: |
2479 | | - logger.warning( |
2480 | | - "Resource Exhausted error on attempt %d/%d: %s. Retrying" |
2481 | | - " in %s seconds...", |
2482 | | - attempt + 1, |
2483 | | - max_retries, |
2484 | | - e, |
2485 | | - 2**attempt, |
2486 | | - ) |
2487 | | - if attempt == max_retries - 1: |
2488 | | - return {"error": f"Resource exhausted after retries: {e}"} |
2489 | | - await asyncio.sleep(2**attempt) |
2490 | 2484 | except Exception as e: # pylint: disable=broad-exception-caught |
2491 | | - logger.error( |
2492 | | - "Unexpected error during agent run on attempt %d/%d: %s", |
2493 | | - attempt + 1, |
2494 | | - max_retries, |
2495 | | - e, |
2496 | | - ) |
2497 | | - if attempt == max_retries - 1: |
2498 | | - return {"error": f"Failed after retries: {e}"} |
2499 | | - await asyncio.sleep(1) |
| 2485 | + backoff = _get_retry_backoff(e, attempt, max_retries, "agent run") |
| 2486 | + if backoff is None: |
| 2487 | + return { |
| 2488 | + "error": f"Failed on attempt {attempt + 1}/{max_retries}: {e}" |
| 2489 | + } |
| 2490 | + await asyncio.sleep(backoff) |
2500 | 2491 | return {"error": f"Failed to get agent run results after {max_retries} retries"} |
2501 | 2492 |
|
2502 | 2493 |
|
|
0 commit comments