""" Shared HTTP clients for performance. Why: - Creating a new httpx.AsyncClient per request is expensive (TLS handshakes, no connection reuse). - For high-frequency lookups (RDAP), we keep one pooled AsyncClient per process. Notes: - Per-request timeouts can still be overridden in client.get(..., timeout=...). - Call close_* on shutdown for clean exit (optional but recommended). """ from __future__ import annotations import asyncio from typing import Optional import httpx _rdap_client: Optional[httpx.AsyncClient] = None _rdap_client_lock = asyncio.Lock() def _rdap_limits() -> httpx.Limits: # Conservative but effective defaults (works well for bursty traffic). return httpx.Limits(max_connections=50, max_keepalive_connections=20, keepalive_expiry=30.0) def _rdap_timeout() -> httpx.Timeout: # Overall timeout can be overridden per request. return httpx.Timeout(15.0, connect=5.0) async def get_rdap_http_client() -> httpx.AsyncClient: """ Get a shared httpx.AsyncClient for RDAP requests. Safe for concurrent use within the same event loop. """ global _rdap_client if _rdap_client is not None and not _rdap_client.is_closed: return _rdap_client async with _rdap_client_lock: if _rdap_client is not None and not _rdap_client.is_closed: return _rdap_client _rdap_client = httpx.AsyncClient( timeout=_rdap_timeout(), follow_redirects=True, limits=_rdap_limits(), headers={ # Be a good citizen; many registries/redirectors are sensitive. "User-Agent": "pounce/1.0 (+https://pounce.ch)", "Accept": "application/rdap+json, application/json", }, ) return _rdap_client async def close_rdap_http_client() -> None: """Close the shared RDAP client (best-effort).""" global _rdap_client if _rdap_client is None: return try: if not _rdap_client.is_closed: await _rdap_client.aclose() finally: _rdap_client = None