-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Closed
Labels
bugSomething isn't workingSomething isn't working
Description
Initial Checks
- I confirm that I'm using the latest version of Pydantic AI
- I confirm that I searched for my issue in https://github.com/pydantic/pydantic-ai/issues before opening this issue
Description
I found one more issue with AsyncTenacityTransport. It appears like the connection is not released back to the pool on retries, see reproducer. It manifests itself as httpx.PoolTimeout when the attempts exceed the connection pool size.
Example Code
#!/usr/bin/env uv run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "pydantic-ai==0.8.0",
# "httpx==0.28.1",
# "tenacity",
# ]
# ///
import asyncio
import threading
import time
from http.server import HTTPServer, BaseHTTPRequestHandler
import httpx
from httpx import AsyncHTTPTransport
from pydantic_ai.retries import AsyncTenacityTransport, wait_retry_after
from pydantic_ai.retries import RetryConfig as PydanticAiRetryConfig
from tenacity import stop_after_attempt, retry_if_exception_type, wait_exponential
class AlwaysReturnHTTP429Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(429)
self.send_header('Retry-After', '1')
self.end_headers()
self.wfile.write(b'Rate limited')
def log_message(self, format, *args):
pass
def start_test_server(port: int = 8429) -> HTTPServer:
server = HTTPServer(('localhost', port), AlwaysReturnHTTP429Handler)
def run_server():
server.serve_forever()
server_thread = threading.Thread(target=run_server, daemon=True)
server_thread.start()
time.sleep(0.1)
return server
def create_retrying_client() -> httpx.AsyncClient:
def validate_response(response: httpx.Response) -> None:
response.raise_for_status()
retry_strategy = PydanticAiRetryConfig(
stop=stop_after_attempt(5),
wait=wait_retry_after(max_wait=5, fallback_strategy=wait_exponential(multiplier=1, max=2)),
retry=retry_if_exception_type(httpx.HTTPStatusError),
reraise=True,
)
transport = AsyncTenacityTransport(
config=retry_strategy,
validate_response=validate_response,
wrapped=AsyncHTTPTransport(
limits=httpx.Limits(
max_connections=2,
max_keepalive_connections=2,
keepalive_expiry=30
)
)
)
return httpx.AsyncClient(transport=transport)
async def make_request(client: httpx.AsyncClient, url: str, request_id: int):
print(f"Starting request {request_id}")
try:
response = await client.get(url)
print(f"Request {request_id} completed with status: {response.status_code}")
return response
except Exception as e:
print(f"Request {request_id} failed after all retries: {e}")
raise
async def main():
print("Starting server that always returns 429...")
server = start_test_server(8429)
test_url = "http://localhost:8429/test"
print("Creating client with 2 connection pool and retry on all requests...")
client = create_retrying_client()
try:
print("Making single request (will retry 5 times)...")
result = await make_request(client, test_url, 1)
if isinstance(result, Exception):
print(f"Request failed: {result}")
else:
print(f"Request completed with status: {result.status_code}")
finally:
await client.aclose()
server.shutdown()
print("Cleanup completed")
if __name__ == "__main__":
asyncio.run(main())Python, Pydantic AI & LLM client version
python 3.13, pydantic-ai 0.8.0
Metadata
Metadata
Assignees
Labels
bugSomething isn't workingSomething isn't working