ForcePilot/backend/package/yuxi/channel/gateway/channel_plugin.py

193 lines
6.7 KiB
Python
Raw Normal View History

import logging
from yuxi.channel.runtime.backoff import BackoffConfig, ErrorBackoff
from yuxi.channel.extensions.base import BaseChannelPlugin
from yuxi.channel.gateway.client import GatewayClient
logger = logging.getLogger(__name__)
DEFAULT_RECONNECT_LEVELS = [1.0, 2.0, 4.0, 8.0, 15.0, 30.0]
class GatewayChannelPlugin(BaseChannelPlugin):
"""使用 Gateway Client SDK 的渠道插件基类。
封装常用连接模式
- 自动重连基于 ErrorBackoff 的状态管理
- 请求重试基于 ErrorBackoff.execute
- 设备身份管理自动生成/注册/刷新
- 生命周期回调on_connect / on_disconnect
用法::
class MyChannelPlugin(GatewayChannelPlugin):
id = "my_channel"
name = "MyChannel"
async def on_connect(self, client: GatewayClient) -> None:
await client.request("channel.start")
async def on_event(self, client: GatewayClient, event) -> None:
...
"""
id: str = ""
name: str = ""
def __init__(
self,
*,
gateway_url: str = "ws://127.0.0.1:9001",
token: str | None = None,
device_token: str | None = None,
request_timeout: float = 30.0,
reconnect_base_ms: float = 1000.0,
reconnect_max_ms: float = 30000.0,
reconnect_factor: float = 2.0,
reconnect_jitter: float = 0.1,
):
self._gateway_url = gateway_url
self._token = token
self._device_token = device_token
self._request_timeout = request_timeout
self._reconnect_base_ms = reconnect_base_ms
self._reconnect_max_ms = reconnect_max_ms
self._reconnect_factor = reconnect_factor
self._reconnect_jitter = reconnect_jitter
self._client: GatewayClient | None = None
self._backoff = ErrorBackoff(
config=BackoffConfig(
base_delay=reconnect_base_ms / 1000.0,
max_delay=reconnect_max_ms / 1000.0,
exponent=reconnect_factor,
jitter=True,
jitter_factor=reconnect_jitter,
max_retries=0,
),
)
@property
def client(self) -> GatewayClient | None:
return self._client
@property
def connected(self) -> bool:
return self._client is not None and self._client.connected
async def start(self, ctx) -> object:
self._client = GatewayClient(
url=self._gateway_url,
token=self._token,
device_token=self._device_token,
request_timeout=self._request_timeout,
reconnect_base_ms=self._reconnect_base_ms,
reconnect_max_ms=self._reconnect_max_ms,
reconnect_factor=self._reconnect_factor,
reconnect_jitter=self._reconnect_jitter,
on_connect=self._on_gw_connect,
on_disconnect=self._on_gw_disconnect,
on_event=self._on_gw_event,
on_error=self._on_gw_error,
on_close=self._on_gw_close,
)
try:
await self._client.start()
except ValueError as e:
logger.error("[%s] Gateway client start failed: %s", self.id, e)
raise
logger.info("[%s] Gateway channel plugin started", self.id)
return self._client
async def stop(self, ctx) -> None:
if self._client:
await self._client.stop()
self._client = None
self._backoff.reset(f"ch-{self.id}")
logger.info("[%s] Gateway channel plugin stopped", self.id)
async def send_request(
self,
method: str,
params: dict | None = None,
*,
timeout: float | None = None,
max_attempts: int = 3,
) -> dict:
"""发送 RPC 请求,带自动重试。"""
if self._client is None:
raise RuntimeError(f"[{self.id}] gateway client not started")
return await self._client.request_with_retry(
method,
params=params,
timeout=timeout,
max_attempts=max_attempts,
)
async def init_device_identity(self) -> str:
"""初始化设备身份,返回 device_token。"""
if self._client is None:
self._client = GatewayClient(url=self._gateway_url, token=self._token)
return await self._client.init_device_identity()
async def refresh_device_token(self) -> str | None:
"""刷新设备令牌。"""
if self._client is None:
return None
return await self._client.refresh_device_token()
def create_backoff(self, name: str, levels: list[float] | None = None) -> ErrorBackoff:
"""创建 ErrorBackoff 实例,供子类管理独立退避状态。"""
return ErrorBackoff(
config=BackoffConfig(
base_delay=levels[0] if levels else 1.0,
max_delay=levels[-1] if levels else 30.0,
exponent=2.0,
jitter=True,
jitter_factor=0.1,
max_retries=0,
),
)
async def on_connect(self, client: GatewayClient) -> None:
"""子类可重写Gateway 连接成功时回调。"""
async def on_disconnect(self, client: GatewayClient, code: int, reason: str) -> None:
"""子类可重写Gateway 断开连接时回调。"""
async def on_event(self, client: GatewayClient, event) -> None:
"""子类可重写:收到 Gateway 事件时回调。"""
async def on_error(self, client: GatewayClient, error: Exception) -> None:
"""子类可重写Gateway 错误时回调。"""
async def _on_gw_connect(self, client: GatewayClient) -> None:
try:
await self.on_connect(client)
except Exception:
logger.exception("[%s] on_connect callback failed", self.id)
async def _on_gw_disconnect(self, client: GatewayClient, code: int, reason: str) -> None:
try:
await self.on_disconnect(client, code, reason)
except Exception:
logger.exception("[%s] on_disconnect callback failed", self.id)
async def _on_gw_event(self, client: GatewayClient, event) -> None:
try:
await self.on_event(client, event)
except Exception:
logger.exception("[%s] on_event callback failed", self.id)
async def _on_gw_error(self, client: GatewayClient, error: Exception) -> None:
try:
await self.on_error(client, error)
except Exception:
logger.exception("[%s] on_error callback failed", self.id)
async def _on_gw_close(self, client: GatewayClient, code: int, reason: str) -> None:
logger.info("[%s] Gateway connection closed: code=%d reason=%s", self.id, code, reason)