From 96c57d16a9fee3e832f711b24c9a3a7d079775dd Mon Sep 17 00:00:00 2001 From: pax Date: Sun, 5 Apr 2026 17:22:30 -0500 Subject: [PATCH] Share HTTP client across all API calls for Windows performance Single shared httpx.AsyncClient for all BooruClient instances (Danbooru, Gelbooru, Moebooru) with connection pooling. E621 gets its own shared client (custom User-Agent required). Site detection also reuses the shared client. Eliminates per-request TLS handshakes on Windows. --- booru_viewer/core/api/base.py | 14 ++++++++------ booru_viewer/core/api/detect.py | 16 +++++++++++----- booru_viewer/core/api/e621.py | 18 +++++++++++------- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/booru_viewer/core/api/base.py b/booru_viewer/core/api/base.py index 62c3fd7..be3b0b4 100644 --- a/booru_viewer/core/api/base.py +++ b/booru_viewer/core/api/base.py @@ -37,6 +37,9 @@ class BooruClient(ABC): api_type: str = "" + # Shared client across all BooruClient instances for connection reuse + _shared_client: httpx.AsyncClient | None = None + def __init__( self, base_url: str, @@ -46,26 +49,25 @@ class BooruClient(ABC): self.base_url = base_url.rstrip("/") self.api_key = api_key self.api_user = api_user - self._client: httpx.AsyncClient | None = None @property def client(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - self._client = httpx.AsyncClient( + if BooruClient._shared_client is None or BooruClient._shared_client.is_closed: + BooruClient._shared_client = httpx.AsyncClient( headers={"User-Agent": USER_AGENT}, follow_redirects=True, timeout=20.0, event_hooks={"request": [self._log_request]}, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), ) - return self._client + return BooruClient._shared_client @staticmethod async def _log_request(request: httpx.Request) -> None: log_connection(str(request.url)) async def close(self) -> None: - if self._client and not self._client.is_closed: - await self._client.aclose() + pass # shared client stays open @abstractmethod async def search( diff --git a/booru_viewer/core/api/detect.py b/booru_viewer/core/api/detect.py index c34c995..418be37 100644 --- a/booru_viewer/core/api/detect.py +++ b/booru_viewer/core/api/detect.py @@ -23,11 +23,17 @@ async def detect_site_type( """ url = url.rstrip("/") - async with httpx.AsyncClient( - headers={"User-Agent": USER_AGENT}, - follow_redirects=True, - timeout=10.0, - ) as client: + from .base import BooruClient as _BC + # Reuse shared client for site detection + if _BC._shared_client is None or _BC._shared_client.is_closed: + _BC._shared_client = httpx.AsyncClient( + headers={"User-Agent": USER_AGENT}, + follow_redirects=True, + timeout=20.0, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), + ) + client = _BC._shared_client + if True: # keep indent level # Try Danbooru / e621 first — /posts.json is a definitive endpoint try: params: dict = {"limit": 1} diff --git a/booru_viewer/core/api/e621.py b/booru_viewer/core/api/e621.py index b465ea1..b7975fe 100644 --- a/booru_viewer/core/api/e621.py +++ b/booru_viewer/core/api/e621.py @@ -15,19 +15,23 @@ log = logging.getLogger("booru") class E621Client(BooruClient): api_type = "e621" + _e621_client: httpx.AsyncClient | None = None + _e621_ua: str = "" + @property def client(self) -> httpx.AsyncClient: - if self._client is None or self._client.is_closed: - # e621 requires a descriptive User-Agent with username - ua = USER_AGENT - if self.api_user: - ua = f"{USER_AGENT} (by {self.api_user} on e621)" - self._client = httpx.AsyncClient( + ua = USER_AGENT + if self.api_user: + ua = f"{USER_AGENT} (by {self.api_user} on e621)" + if E621Client._e621_client is None or E621Client._e621_client.is_closed or E621Client._e621_ua != ua: + E621Client._e621_ua = ua + E621Client._e621_client = httpx.AsyncClient( headers={"User-Agent": ua}, follow_redirects=True, timeout=20.0, + limits=httpx.Limits(max_connections=10, max_keepalive_connections=5), ) - return self._client + return E621Client._e621_client async def search( self, tags: str = "", page: int = 1, limit: int = DEFAULT_PAGE_SIZE