"""SSH Proxy Connection Pool — 串行化所有SSH中转请求,避免并发资源竞争。 所有需要走中转的数据源统一调用 ssh_proxy_fetch(url), 内部通过队列串行化SSH连接,避免多线程/多进程并发导致的连接冲突。 """ import json import logging import subprocess import threading import time from queue import Queue, Empty from typing import Optional logger = logging.getLogger(__name__) # SSH配置缓存 _ssh_config: Optional[tuple] = None _config_lock = threading.Lock() def _get_ssh_config() -> tuple: """获取SSH配置(host, port, password),带缓存。""" global _ssh_config if _ssh_config is not None: return _ssh_config with _config_lock: if _ssh_config is not None: return _ssh_config from .stockstats_utils import _get_ssh_config as _load_config _ssh_config = _load_config() return _ssh_config class SSHProxyPool: """SSH代理连接池 — 串行化所有SSH请求。""" def __init__(self): self._queue = Queue() self._lock = threading.Lock() self._initialized = False def _ensure_initialized(self): """确保连接池已初始化。""" if self._initialized: return with self._lock: if self._initialized: return # 放一个占位符,表示可以发起请求 self._queue.put(True) self._initialized = True def fetch(self, url: str, timeout: int = 30) -> str: """通过SSH代理获取URL内容,串行化执行。 Args: url: 要获取的URL timeout: 超时时间(秒) Returns: URL响应内容字符串 Raises: RuntimeError: SSH代理失败时抛出 """ self._ensure_initialized() # 等待轮到自己(最多等timeout秒) try: self._queue.get(timeout=timeout) except Empty: raise RuntimeError(f"SSH proxy queue timeout after {timeout}s") try: host, port, password = _get_ssh_config() cmd = [ "sshpass", "-p", password, "ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=15", "-o", "ServerAliveInterval=10", "-p", str(port), f"root@{host}", f'curl -sL --max-time {timeout} "{url}"' ] result = subprocess.run( cmd, capture_output=True, text=True, timeout=timeout + 10 # 给SSH额外时间 ) if result.returncode != 0: raise RuntimeError(f"SSH proxy failed (rc={result.returncode}): {result.stderr[:200]}") if not result.stdout or not result.stdout.strip(): raise RuntimeError("SSH proxy returned empty response") return result.stdout except subprocess.TimeoutExpired: raise RuntimeError(f"SSH proxy subprocess timeout after {timeout}s") finally: # 无论成功失败,都放回占位符,让下一个请求执行 self._queue.put(True) def fetch_json(self, url: str, timeout: int = 30) -> dict: """通过SSH代理获取JSON响应。""" text = self.fetch(url, timeout) try: return json.loads(text) except json.JSONDecodeError as e: raise RuntimeError(f"SSH proxy returned invalid JSON: {e}") # 全局单例 _pool = SSHProxyPool() def ssh_proxy_fetch(url: str, timeout: int = 30) -> str: """通过SSH代理获取URL内容(串行化)。""" return _pool.fetch(url, timeout) def ssh_proxy_fetch_json(url: str, timeout: int = 30) -> dict: """通过SSH代理获取JSON响应(串行化)。""" return _pool.fetch_json(url, timeout)