connection_pool.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. """
  2. 豆包API连接池模块
  3. """
  4. import asyncio
  5. import time
  6. from typing import Optional
  7. from asyncio import Queue
  8. from loguru import logger
  9. from doubao_api.realtime_dialog_client import RealtimeDialogClient
  10. from config import settings
  11. class DoubaoConnectionPool:
  12. """豆包API连接池"""
  13. def __init__(
  14. self,
  15. pool_size: int = None,
  16. max_idle_time: int = None
  17. ):
  18. self.pool_size = pool_size or settings.CONNECTION_POOL_SIZE
  19. self.max_idle_time = max_idle_time or settings.CONNECTION_MAX_IDLE_TIME
  20. # 空闲连接队列
  21. self.idle_connections: Queue = Queue(maxsize=self.pool_size)
  22. # 活跃连接集合
  23. self.active_connections: set = set()
  24. # 连接健康状态
  25. self.connection_health: dict = {}
  26. # 初始化标志
  27. self.is_initialized = False
  28. async def initialize(self):
  29. """初始化连接池 - 预创建所有连接"""
  30. if self.is_initialized:
  31. logger.warning("连接池已经初始化")
  32. return
  33. logger.info(f"开始初始化连接池,创建 {self.pool_size} 个连接...")
  34. tasks = []
  35. for i in range(self.pool_size):
  36. task = asyncio.create_task(self._create_connection(i))
  37. tasks.append(task)
  38. connections = await asyncio.gather(*tasks, return_exceptions=True)
  39. success_count = 0
  40. for i, conn in enumerate(connections):
  41. if isinstance(conn, Exception):
  42. logger.error(f"连接 #{i} 创建失败: {conn}")
  43. elif conn:
  44. await self.idle_connections.put(conn)
  45. self.connection_health[id(conn)] = {
  46. 'created_at': time.time(),
  47. 'last_used': time.time(),
  48. 'request_count': 0
  49. }
  50. success_count += 1
  51. logger.info(f"连接池初始化完成,成功创建 {success_count}/{self.pool_size} 个连接")
  52. self.is_initialized = True
  53. # 启动健康检查任务
  54. asyncio.create_task(self._health_check_loop())
  55. async def _create_connection(self, index: int):
  56. """创建单个豆包API连接"""
  57. try:
  58. logger.info(f"创建豆包API连接 #{index}...")
  59. # 检查API配置
  60. if not settings.DOUBAO_API_KEY or not settings.DOUBAO_APP_ID:
  61. logger.warning(f"连接 #{index}: 豆包API密钥未配置,创建模拟连接")
  62. return await self._create_mock_connection(index)
  63. # 创建真实的豆包API客户端
  64. client = RealtimeDialogClient(
  65. api_key=settings.DOUBAO_API_KEY,
  66. app_id=settings.DOUBAO_APP_ID,
  67. base_url=settings.DOUBAO_API_URL or "wss://openspeech.bytedance.com/api/v3/realtime/dialogue",
  68. output_audio_format="ogg_opus", # 返回OGG/Opus格式
  69. recv_timeout=settings.DOUBAO_RECV_TIMEOUT,
  70. connect_timeout=settings.DOUBAO_CONNECT_TIMEOUT
  71. )
  72. # 连接并初始化
  73. await client.connect()
  74. logger.success(f"连接 #{index} 创建成功 (真实API)")
  75. return client
  76. except Exception as e:
  77. logger.error(f"连接 #{index} 创建失败: {e}")
  78. logger.warning(f"连接 #{index}: 回退到模拟连接")
  79. return await self._create_mock_connection(index)
  80. async def _create_mock_connection(self, index: int):
  81. """创建模拟连接(用于测试)"""
  82. logger.info(f"创建模拟连接 #{index}...")
  83. class MockConnection:
  84. def __init__(self, conn_id):
  85. self.conn_id = conn_id
  86. self.connected = True
  87. self.session_id = f"mock_session_{conn_id}"
  88. def is_connected(self):
  89. return self.connected
  90. async def send_audio(self, audio_data):
  91. """发送音频数据"""
  92. logger.debug(f"[模拟] 发送音频: {len(audio_data)} bytes")
  93. await asyncio.sleep(0.01)
  94. async def receive_audio(self):
  95. """接收音频数据"""
  96. await asyncio.sleep(0.1)
  97. # 返回模拟的OGG音频数据
  98. mock_audio = b"OggS" + b"\x00" * 100 # 模拟OGG头
  99. logger.debug(f"[模拟] 收到音频: {len(mock_audio)} bytes")
  100. return mock_audio
  101. async def close(self):
  102. self.connected = False
  103. logger.debug(f"[模拟] 连接已关闭")
  104. return MockConnection(index)
  105. async def acquire(self, timeout: int = 5) -> Optional:
  106. """获取一个可用连接"""
  107. try:
  108. # 从空闲队列获取连接
  109. connection = await asyncio.wait_for(
  110. self.idle_connections.get(),
  111. timeout=timeout
  112. )
  113. # 移动到活跃集合
  114. self.active_connections.add(connection)
  115. # 更新健康状态
  116. conn_id = id(connection)
  117. if conn_id in self.connection_health:
  118. self.connection_health[conn_id]['last_used'] = time.time()
  119. self.connection_health[conn_id]['request_count'] += 1
  120. logger.debug(f"获取连接成功,当前活跃连接: {len(self.active_connections)}")
  121. return connection
  122. except asyncio.TimeoutError:
  123. logger.warning("连接池已耗尽,等待超时")
  124. return None
  125. except Exception as e:
  126. logger.error(f"获取连接失败: {e}")
  127. return None
  128. async def release(self, connection):
  129. """归还连接到池中"""
  130. if connection in self.active_connections:
  131. self.active_connections.remove(connection)
  132. # 检查连接是否健康
  133. if await self._is_connection_healthy(connection):
  134. await self.idle_connections.put(connection)
  135. logger.debug(f"连接已归还,当前空闲连接: {self.idle_connections.qsize()}")
  136. else:
  137. # 连接不健康,先关闭旧连接
  138. reason = "致命错误" if hasattr(connection, 'has_fatal_error') and connection.has_fatal_error else "健康检查失败"
  139. logger.warning(f"连接不健康 ({reason}),销毁并重新创建...")
  140. try:
  141. await connection.close()
  142. except:
  143. pass
  144. # 重新创建连接
  145. new_conn = await self._create_connection(-1)
  146. if new_conn:
  147. await self.idle_connections.put(new_conn)
  148. self.connection_health[id(new_conn)] = {
  149. 'created_at': time.time(),
  150. 'last_used': time.time(),
  151. 'request_count': 0
  152. }
  153. async def _is_connection_healthy(self, connection) -> bool:
  154. """检查连接健康状态"""
  155. try:
  156. # 检查是否有致命错误
  157. if hasattr(connection, 'has_fatal_error') and connection.has_fatal_error:
  158. logger.warning(f"连接有致命错误: {connection.session_id}")
  159. return False
  160. # 检查连接是否还活着
  161. if not connection or not connection.is_connected():
  162. return False
  163. # 检查空闲时间
  164. conn_id = id(connection)
  165. if conn_id in self.connection_health:
  166. idle_time = time.time() - self.connection_health[conn_id]['last_used']
  167. if idle_time > self.max_idle_time:
  168. logger.warning(f"连接空闲时间过长: {idle_time}s")
  169. return False
  170. return True
  171. except Exception as e:
  172. logger.error(f"健康检查失败: {e}")
  173. return False
  174. async def _health_check_loop(self):
  175. """定期健康检查"""
  176. while True:
  177. try:
  178. await asyncio.sleep(settings.CONNECTION_HEALTH_CHECK_INTERVAL)
  179. idle_count = self.idle_connections.qsize()
  180. active_count = len(self.active_connections)
  181. logger.info(
  182. f"连接池健康检查 - "
  183. f"空闲: {idle_count}, "
  184. f"活跃: {active_count}, "
  185. f"总计: {idle_count + active_count}/{self.pool_size}"
  186. )
  187. # 🔧 主动清理空闲连接池中的失效连接
  188. unhealthy_count = 0
  189. temp_connections = []
  190. # 取出所有空闲连接检查
  191. while not self.idle_connections.empty():
  192. try:
  193. conn = await asyncio.wait_for(self.idle_connections.get(), timeout=1)
  194. temp_connections.append(conn)
  195. except asyncio.TimeoutError:
  196. break
  197. # 检查并重新放回健康的连接
  198. for conn in temp_connections:
  199. if await self._is_connection_healthy(conn):
  200. await self.idle_connections.put(conn)
  201. else:
  202. unhealthy_count += 1
  203. logger.warning(f"清理不健康的空闲连接: {conn.session_id}")
  204. try:
  205. await conn.close()
  206. except:
  207. pass
  208. # 创建新连接补充
  209. new_conn = await self._create_connection(-1)
  210. if new_conn:
  211. await self.idle_connections.put(new_conn)
  212. self.connection_health[id(new_conn)] = {
  213. 'created_at': time.time(),
  214. 'last_used': time.time(),
  215. 'request_count': 0
  216. }
  217. if unhealthy_count > 0:
  218. logger.info(f"🔧 清理了 {unhealthy_count} 个不健康的连接并重新创建")
  219. except Exception as e:
  220. logger.error(f"健康检查循环错误: {e}")
  221. def get_pool_status(self) -> dict:
  222. """获取连接池状态"""
  223. return {
  224. 'pool_size': self.pool_size,
  225. 'idle_connections': self.idle_connections.qsize(),
  226. 'active_connections': len(self.active_connections),
  227. 'total_connections': self.idle_connections.qsize() + len(self.active_connections),
  228. 'health_check_interval': settings.CONNECTION_HEALTH_CHECK_INTERVAL,
  229. 'max_idle_time': self.max_idle_time,
  230. }
  231. async def close_all(self):
  232. """关闭所有连接"""
  233. logger.info("关闭连接池...")
  234. # 关闭活跃连接
  235. for conn in self.active_connections:
  236. try:
  237. await conn.close()
  238. except Exception as e:
  239. logger.error(f"关闭活跃连接失败: {e}")
  240. # 关闭空闲连接
  241. while not self.idle_connections.empty():
  242. try:
  243. conn = await self.idle_connections.get()
  244. await conn.close()
  245. except Exception as e:
  246. logger.error(f"关闭空闲连接失败: {e}")
  247. self.active_connections.clear()
  248. self.connection_health.clear()
  249. self.is_initialized = False
  250. logger.info("连接池已关闭")
  251. # 创建全局连接池实例
  252. connection_pool = DoubaoConnectionPool()