from __future__ import annotations import asyncio import time from dataclasses import dataclass, field from typing import Any from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient from yuxi.utils.logging_config import logger @dataclass class PooledClient: client: BlueBubblesClient last_used: float = field(default_factory=time.monotonic) in_use: bool = False request_count: int = 0 class ClientPool: MAX_IDLE_SECONDS = 300 CLEANUP_INTERVAL = 60.0 DEFAULT_POOL_SIZE = 5 def __init__(self, pool_size: int | None = None): self._pool: list[PooledClient] = [] self._max_size = pool_size or self.DEFAULT_POOL_SIZE self._lock = asyncio.Lock() self._cleanup_task: asyncio.Task | None = None self._config: dict[str, Any] = {} def configure( self, server_url: str, password: str, timeout: float = 30.0, allow_private_network: bool = False, ) -> None: self._config = { "server_url": server_url, "password": password, "timeout": timeout, "allow_private_network": allow_private_network, } async def acquire(self) -> BlueBubblesClient: async with self._lock: for entry in self._pool: if not entry.in_use: entry.in_use = True entry.last_used = time.monotonic() entry.request_count += 1 return entry.client client = self._create_client() entry = PooledClient(client=client, in_use=True, request_count=1) self._pool.append(entry) return client async def release(self, client: BlueBubblesClient) -> None: async with self._lock: for entry in self._pool: if entry.client is client: entry.in_use = False entry.last_used = time.monotonic() return def _create_client(self) -> BlueBubblesClient: return BlueBubblesClient( server_url=self._config.get("server_url", "http://localhost:1234"), password=self._config.get("password", ""), timeout=self._config.get("timeout", 30.0), allow_private_network=self._config.get("allow_private_network", False), ) async def start_cleanup(self) -> None: if self._cleanup_task and not self._cleanup_task.done(): return self._cleanup_task = asyncio.create_task(self._cleanup_loop()) async def stop(self) -> None: if self._cleanup_task and not self._cleanup_task.done(): self._cleanup_task.cancel() try: await self._cleanup_task except asyncio.CancelledError: pass self._cleanup_task = None async with self._lock: for entry in self._pool: try: await entry.client.close() except Exception: pass self._pool.clear() async def _cleanup_loop(self) -> None: while True: await asyncio.sleep(self.CLEANUP_INTERVAL) await self._cleanup_idle() async def _cleanup_idle(self) -> None: now = time.monotonic() async with self._lock: idle_entries = [e for e in self._pool if not e.in_use and now - e.last_used > self.MAX_IDLE_SECONDS] for entry in idle_entries: try: await entry.client.close() except Exception: pass self._pool.remove(entry) logger.debug("[BlueBubbles] Pool: removed idle client") @property def size(self) -> int: return len(self._pool) @property def available(self) -> int: return sum(1 for e in self._pool if not e.in_use)