| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- import asyncio
- import json
- from dataclasses import dataclass
- from typing import Any
- from contextlib import asynccontextmanager
- class JsonRpcError(Exception):
- def __init__(self, code: int, message: str, data: Any = None):
- self.code = code
- self.message = message
- self.data = data
- super().__init__(f"RPC Error {code}: {message}")
- class JsonRpcConnection:
- """Single JSON-RPC connection over TCP"""
- def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
- self.reader = reader
- self.writer = writer
- self.request_id = 0
- self._lock = asyncio.Lock()
- self._closed = False
- @property
- def is_closed(self) -> bool:
- return self._closed or self.writer.is_closing()
- async def call(self, method: str, params: Any = None, timeout: float = 30.0) -> Any:
- if self.is_closed:
- raise ConnectionError("Connection is closed")
- async with self._lock:
- self.request_id += 1
- request = {
- "jsonrpc": "2.0",
- "method": method,
- "id": self.request_id,
- "params": params,
- }
- message = json.dumps(request) + "\n"
- try:
- self.writer.write(message.encode("utf-8"))
- await self.writer.drain()
- response_line = await asyncio.wait_for(
- self.reader.readline(),
- timeout=timeout
- )
- except asyncio.TimeoutError:
- self._closed = True
- raise TimeoutError(f"RPC call '{method}' timed out after {timeout}s")
- except (ConnectionError, OSError) as e:
- self._closed = True
- raise ConnectionError(f"Connection lost: {e}")
- if not response_line:
- self._closed = True
- raise ConnectionError("Connection closed by server")
- response = json.loads(response_line.decode("utf-8"))
- if "error" in response and response["error"]:
- err = response["error"]
- raise JsonRpcError(
- err.get("code", -1),
- err.get("message", "Unknown error"),
- err.get("data")
- )
- return response.get("result")
- async def close(self):
- if not self._closed:
- self._closed = True
- self.writer.close()
- try:
- await self.writer.wait_closed()
- except Exception:
- pass
- class JsonRpcPool:
- """Connection pool with automatic reconnection"""
- def __init__(
- self,
- host: str,
- port: int,
- min_connections: int = 5,
- max_connections: int = 20,
- ):
- self.host = host
- self.port = port
- self.min_connections = min_connections
- self.max_connections = max_connections
- self._pool: asyncio.Queue[JsonRpcConnection] = None
- self._semaphore: asyncio.Semaphore = None
- self._connection_count = 0
- self._lock = asyncio.Lock()
- self._closed = False
- async def start(self):
- """Initialize the pool with minimum connections"""
- self._pool = asyncio.Queue()
- self._semaphore = asyncio.Semaphore(self.max_connections)
- self._connection_count = 0
- for _ in range(self.min_connections):
- try:
- conn = await self._create_connection()
- await self._pool.put(conn)
- except Exception as e:
- print(f"Warning: Failed to create initial connection: {e}")
- async def _create_connection(self) -> JsonRpcConnection:
- reader, writer = await asyncio.open_connection(self.host, self.port)
- async with self._lock:
- self._connection_count += 1
- return JsonRpcConnection(reader, writer)
- async def _destroy_connection(self, conn: JsonRpcConnection):
- await conn.close()
- async with self._lock:
- self._connection_count -= 1
- @asynccontextmanager
- async def connection(self):
- """Acquire a connection from the pool"""
- if self._closed:
- raise RuntimeError("Pool's closed")
- conn = None
- async with self._semaphore:
- # Try to get an existing connection
- while not self._pool.empty():
- conn = await self._pool.get()
- if not conn.is_closed:
- break
- await self._destroy_connection(conn)
- conn = None
- # Create new if needed
- if conn is None:
- conn = await self._create_connection()
- try:
- yield conn
- except (ConnectionError, TimeoutError):
- # Connection is bad, don't return to pool
- await self._destroy_connection(conn)
- raise
- else:
- # Return healthy connection to pool
- if not conn.is_closed:
- await self._pool.put(conn)
- else:
- await self._destroy_connection(conn)
- async def call(self, method: str, params: Any = None, timeout: float = 30.0) -> Any:
- """Make an RPC call using a pooled connection"""
- async with self.connection() as conn:
- return await conn.call(method, params, timeout)
- async def close(self):
- """Close all connections"""
- self._closed = True
- while not self._pool.empty():
- conn = await self._pool.get()
- await self._destroy_connection(conn)
|