2026-07-02 03:22:12 +08:00
|
|
|
|
"""入站传输管理器。
|
|
|
|
|
|
|
|
|
|
|
|
统一管理 PullerWorker 和 StreamWorker 的生命周期,订阅账号上下线事件,
|
|
|
|
|
|
动态启动/停止账号的传输任务。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
import uuid
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
|
|
|
|
from typing import Any
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.application.transport.base_worker import (
|
|
|
|
|
|
TransportConfig,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.application.transport.puller_worker import PullerWorker
|
|
|
|
|
|
from yuxi.channels.application.transport.stream_worker import StreamWorker
|
|
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
2026-07-03 19:18:13 +08:00
|
|
|
|
from yuxi.channels.contract.dtos.health import TransportHealthSnapshot
|
2026-07-02 03:22:12 +08:00
|
|
|
|
from yuxi.channels.contract.dtos.plugin import DomainEvent, EventHandler
|
|
|
|
|
|
from yuxi.channels.contract.plugin.extension_point import EventSubscription
|
|
|
|
|
|
from yuxi.channels.contract.plugin.manifest import FailurePolicy
|
|
|
|
|
|
from yuxi.channels.contract.ports.driven import (
|
|
|
|
|
|
ConfigPort,
|
|
|
|
|
|
LoggerPort,
|
|
|
|
|
|
PersistencePort,
|
|
|
|
|
|
)
|
|
|
|
|
|
from yuxi.channels.core.registry import PluginRegistry
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
__all__ = ["TransportManager"]
|
|
|
|
|
|
|
|
|
|
|
|
# 管理循环的轮询间隔(秒):仅用于等待取消信号,不参与业务逻辑
|
|
|
|
|
|
_MANAGER_LOOP_INTERVAL_S: float = 1.0
|
|
|
|
|
|
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
class TransportManager:
|
|
|
|
|
|
"""入站传输全局管理器。
|
|
|
|
|
|
|
|
|
|
|
|
单例模式管理 PullerWorker 和 StreamWorker,订阅 ChannelAccountOnline/Offline
|
|
|
|
|
|
事件,动态启动/停止对应账号的传输任务。P0 阶段仅支持 Puller 模式。
|
|
|
|
|
|
|
|
|
|
|
|
职责:
|
|
|
|
|
|
- 从 PluginRegistry 动态构建 puller/stream 适配器注册表
|
|
|
|
|
|
- 创建并管理 PullerWorker、StreamWorker 单例
|
|
|
|
|
|
- 订阅账号上下线事件,驱动 Worker 启动/停止账号任务
|
|
|
|
|
|
- 提供全局健康状态查询
|
|
|
|
|
|
- 优雅关停所有传输任务
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
plugin_registry: PluginRegistry,
|
|
|
|
|
|
persistence_port: PersistencePort,
|
|
|
|
|
|
config_port: ConfigPort,
|
|
|
|
|
|
event_bus: Any,
|
|
|
|
|
|
circuit_breaker: Any,
|
|
|
|
|
|
logger: LoggerPort,
|
|
|
|
|
|
message_deliverer: Callable[[Any], Awaitable[Any]],
|
|
|
|
|
|
transport_config: TransportConfig | None = None,
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
"""初始化 TransportManager。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
plugin_registry: 插件注册表,用于动态获取渠道适配器。
|
|
|
|
|
|
persistence_port: 持久化端口,用于游标持久化等。
|
|
|
|
|
|
config_port: 配置端口。
|
|
|
|
|
|
event_bus: 事件总线,用于订阅/发布事件。
|
|
|
|
|
|
circuit_breaker: 熔断器实例。
|
|
|
|
|
|
logger: 日志端口。
|
|
|
|
|
|
message_deliverer: 入站消息投递回调。
|
|
|
|
|
|
transport_config: 传输配置,为 None 时使用默认配置。
|
|
|
|
|
|
"""
|
|
|
|
|
|
self._plugin_registry = plugin_registry
|
|
|
|
|
|
self._persistence_port = persistence_port
|
|
|
|
|
|
self._config_port = config_port
|
|
|
|
|
|
self._event_bus = event_bus
|
|
|
|
|
|
self._circuit_breaker = circuit_breaker
|
|
|
|
|
|
self._logger = logger
|
|
|
|
|
|
self._message_deliverer = message_deliverer
|
|
|
|
|
|
self._config = transport_config or TransportConfig()
|
|
|
|
|
|
|
|
|
|
|
|
self._puller_registry: dict[ChannelType, Any] = {}
|
|
|
|
|
|
self._stream_connector_registry: dict[ChannelType, Any] = {}
|
|
|
|
|
|
self._puller_worker: PullerWorker | None = None
|
|
|
|
|
|
self._stream_worker: StreamWorker | None = None
|
|
|
|
|
|
self._manager_task: asyncio.Task[None] | None = None
|
|
|
|
|
|
self._running: bool = False
|
|
|
|
|
|
self._plugin_id = "transport-manager"
|
|
|
|
|
|
|
|
|
|
|
|
async def start(self) -> None:
|
|
|
|
|
|
"""启动传输管理器。
|
|
|
|
|
|
|
|
|
|
|
|
- 构建 puller_registry 和 stream_connector_registry
|
|
|
|
|
|
- 创建 PullerWorker 和 StreamWorker 实例
|
|
|
|
|
|
- 启动 Worker
|
|
|
|
|
|
- 订阅账号上下线事件
|
|
|
|
|
|
- 启动管理循环任务
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self._running:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
trace_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
|
|
|
puller_registry: dict[ChannelType, Any] = {}
|
|
|
|
|
|
stream_connector_registry: dict[ChannelType, Any] = {}
|
|
|
|
|
|
|
|
|
|
|
|
for channel_type, plugin_da in self._plugin_registry.listPluginAdapters():
|
|
|
|
|
|
if plugin_da.puller_adapters:
|
|
|
|
|
|
puller_registry[channel_type] = plugin_da.puller_adapters[0]
|
|
|
|
|
|
if plugin_da.stream_connector_adapters:
|
|
|
|
|
|
stream_connector_registry[channel_type] = plugin_da.stream_connector_adapters[0]
|
|
|
|
|
|
|
|
|
|
|
|
self._puller_registry = puller_registry
|
|
|
|
|
|
self._stream_connector_registry = stream_connector_registry
|
|
|
|
|
|
|
|
|
|
|
|
self._puller_worker = PullerWorker(
|
|
|
|
|
|
message_deliverer=self._message_deliverer,
|
|
|
|
|
|
logger=self._logger,
|
|
|
|
|
|
config_port=self._config_port,
|
|
|
|
|
|
event_publisher=self._event_bus,
|
|
|
|
|
|
circuit_breaker=self._circuit_breaker,
|
|
|
|
|
|
persistence_port=self._persistence_port,
|
|
|
|
|
|
config=self._config,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
self._stream_worker = StreamWorker(
|
|
|
|
|
|
message_deliverer=self._message_deliverer,
|
|
|
|
|
|
logger=self._logger,
|
|
|
|
|
|
config_port=self._config_port,
|
|
|
|
|
|
event_publisher=self._event_bus,
|
|
|
|
|
|
circuit_breaker=self._circuit_breaker,
|
|
|
|
|
|
config=self._config,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
await self._puller_worker.start()
|
|
|
|
|
|
await self._stream_worker.start()
|
|
|
|
|
|
|
|
|
|
|
|
self._registerEventHandlers()
|
|
|
|
|
|
|
|
|
|
|
|
self._running = True
|
2026-07-03 19:18:13 +08:00
|
|
|
|
self._manager_task = asyncio.create_task(
|
|
|
|
|
|
self._manager_loop(),
|
|
|
|
|
|
name="transport-manager-loop",
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"transport manager started",
|
|
|
|
|
|
trace_id=trace_id,
|
2026-07-04 00:14:56 +08:00
|
|
|
|
puller_channels=[ct for ct in puller_registry.keys()],
|
|
|
|
|
|
stream_channels=[ct for ct in stream_connector_registry.keys()],
|
2026-07-02 03:22:12 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def stop(self, timeout: float = 5.0) -> None:
|
|
|
|
|
|
"""停止传输管理器。
|
|
|
|
|
|
|
|
|
|
|
|
- 取消事件订阅
|
|
|
|
|
|
- 取消管理循环任务
|
|
|
|
|
|
- 停止所有 Worker
|
|
|
|
|
|
- 等待所有任务完成
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
timeout: 等待 Worker 停止的超时时间(秒)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self._running:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
trace_id = str(uuid.uuid4())
|
|
|
|
|
|
self._running = False
|
|
|
|
|
|
|
|
|
|
|
|
self._unregisterEventHandlers()
|
|
|
|
|
|
|
|
|
|
|
|
if self._manager_task is not None and not self._manager_task.done():
|
|
|
|
|
|
self._manager_task.cancel()
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self._manager_task
|
|
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
self._manager_task = None
|
|
|
|
|
|
|
|
|
|
|
|
if self._puller_worker is not None:
|
|
|
|
|
|
await self._puller_worker.stop(timeout)
|
|
|
|
|
|
self._puller_worker = None
|
|
|
|
|
|
|
|
|
|
|
|
if self._stream_worker is not None:
|
|
|
|
|
|
await self._stream_worker.stop(timeout)
|
|
|
|
|
|
self._stream_worker = None
|
|
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"transport manager stopped",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-03 19:18:13 +08:00
|
|
|
|
def getHealth(self) -> TransportHealthSnapshot:
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""返回传输引擎健康状态(同步,向后兼容)。
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
``TransportHealthSnapshot``,包含 running 状态和各 Worker 健康
|
|
|
|
|
|
子状态(``WorkerHealthSnapshot``)。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
2026-07-03 19:18:13 +08:00
|
|
|
|
return TransportHealthSnapshot(
|
|
|
|
|
|
running=self._running,
|
|
|
|
|
|
puller=self._puller_worker.getHealth() if self._puller_worker else None,
|
|
|
|
|
|
stream=self._stream_worker.getHealth() if self._stream_worker else None,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def getTransportHealth(self) -> TransportHealthSnapshot:
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""返回传输引擎健康状态(实现 TransportHealthPort,FR-18)。
|
|
|
|
|
|
|
|
|
|
|
|
供 ``HealthAggregator`` 通过 ``TransportHealthPort`` 聚合,暴露每个
|
|
|
|
|
|
账号的运行状态(running/backoff/stopped/error)。实现幂等、无副作用,
|
|
|
|
|
|
不抛异常阻塞调用方。
|
|
|
|
|
|
|
|
|
|
|
|
返回:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
``TransportHealthSnapshot``,结构见 ``TransportHealthPort`` 协议
|
|
|
|
|
|
文档。异常时降级返回 ``puller`` / ``stream`` 为 ``None`` 的快照。
|
2026-07-02 03:22:12 +08:00
|
|
|
|
"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return self.getHealth()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
# 健康检查不得抛异常阻塞调用方(与 HealthAggregator 超时降级策略一致)
|
|
|
|
|
|
await self._logger.warn(
|
|
|
|
|
|
"transport health query failed, returning empty state",
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
2026-07-03 19:18:13 +08:00
|
|
|
|
return TransportHealthSnapshot(
|
|
|
|
|
|
running=self._running,
|
|
|
|
|
|
puller=None,
|
|
|
|
|
|
stream=None,
|
|
|
|
|
|
)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
async def _manager_loop(self) -> None:
|
|
|
|
|
|
"""管理循环,等待取消信号。"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
while self._running:
|
2026-07-03 19:18:13 +08:00
|
|
|
|
await asyncio.sleep(_MANAGER_LOOP_INTERVAL_S)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
except asyncio.CancelledError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def _registerEventHandlers(self) -> None:
|
|
|
|
|
|
"""注册事件处理器。"""
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type="ChannelAccountOnline",
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_account_online),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type="ChannelAccountOffline",
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_account_offline),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type="ChannelDegraded",
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_channel_degraded),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type="ChannelRecovered",
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_channel_recovered),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type="TransportErrorOccurred",
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_transport_error),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
# 配置热更新订阅(FR-04):transport.* 配置变更时应用 hot/restart 策略
|
|
|
|
|
|
for config_event in ("ConfigChanged", "ConfigRollback"):
|
|
|
|
|
|
self._event_bus.register(
|
|
|
|
|
|
self._plugin_id,
|
|
|
|
|
|
EventSubscription(
|
|
|
|
|
|
event_type=config_event,
|
|
|
|
|
|
handler=_AccountEventHandler(self._on_config_changed),
|
|
|
|
|
|
priority=100,
|
2026-07-02 18:27:06 +08:00
|
|
|
|
failure_policy=FailurePolicy.DEGRADE,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _unregisterEventHandlers(self) -> None:
|
|
|
|
|
|
"""取消事件订阅。"""
|
|
|
|
|
|
self._event_bus.unregister(self._plugin_id)
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_account_online(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理账号上线事件。
|
|
|
|
|
|
|
|
|
|
|
|
从事件 payload 获取 channel_type 和 account_id,查找适配器,
|
|
|
|
|
|
启动对应账号的 Puller 和 Stream 任务。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: ChannelAccountOnline 事件。
|
|
|
|
|
|
"""
|
2026-07-03 19:18:13 +08:00
|
|
|
|
trace_id = event.trace_id or str(uuid.uuid4())
|
|
|
|
|
|
channel_type_raw = event.payload.get("channel_type")
|
|
|
|
|
|
account_id = event.payload.get("account_id")
|
|
|
|
|
|
if channel_type_raw is None or account_id is None:
|
|
|
|
|
|
await self._logger.warn(
|
|
|
|
|
|
"account online event missing required fields, skip",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
channel_type=channel_type_raw,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
channel_type = ChannelType(channel_type_raw)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"received account online event, starting transport",
|
|
|
|
|
|
trace_id=trace_id,
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=channel_type,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if self._puller_worker is not None:
|
|
|
|
|
|
adapter = self._puller_registry.get(channel_type)
|
|
|
|
|
|
if adapter is not None:
|
|
|
|
|
|
await self._puller_worker.start_account(channel_type, account_id, adapter)
|
|
|
|
|
|
|
|
|
|
|
|
if self._stream_worker is not None:
|
|
|
|
|
|
adapter = self._stream_connector_registry.get(channel_type)
|
|
|
|
|
|
if adapter is not None:
|
|
|
|
|
|
await self._stream_worker.start_account(channel_type, account_id, adapter)
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_account_offline(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理账号下线事件。
|
|
|
|
|
|
|
|
|
|
|
|
停止对应账号的所有传输任务。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: ChannelAccountOffline 事件。
|
|
|
|
|
|
"""
|
2026-07-03 19:18:13 +08:00
|
|
|
|
trace_id = event.trace_id or str(uuid.uuid4())
|
|
|
|
|
|
channel_type_raw = event.payload.get("channel_type")
|
|
|
|
|
|
account_id = event.payload.get("account_id")
|
|
|
|
|
|
if channel_type_raw is None or account_id is None:
|
|
|
|
|
|
await self._logger.warn(
|
|
|
|
|
|
"account offline event missing required fields, skip",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
channel_type=channel_type_raw,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
channel_type = ChannelType(channel_type_raw)
|
2026-07-02 03:22:12 +08:00
|
|
|
|
reason = event.payload.get("reason", "offline")
|
|
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"received account offline event, stopping transport",
|
|
|
|
|
|
trace_id=trace_id,
|
2026-07-04 00:14:56 +08:00
|
|
|
|
channel_type=channel_type,
|
2026-07-02 03:22:12 +08:00
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
reason=reason,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if self._puller_worker is not None:
|
|
|
|
|
|
await self._puller_worker.stop_account(channel_type, account_id, reason)
|
|
|
|
|
|
|
|
|
|
|
|
if self._stream_worker is not None:
|
|
|
|
|
|
await self._stream_worker.stop_account(channel_type, account_id, reason)
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_channel_degraded(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理渠道降级事件。
|
|
|
|
|
|
|
|
|
|
|
|
P0 阶段留空,P1 阶段实现降级逻辑(如暂停非关键任务、调整轮询间隔等)。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: ChannelDegraded 事件。
|
|
|
|
|
|
"""
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_channel_recovered(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理渠道恢复事件。
|
|
|
|
|
|
|
|
|
|
|
|
P0 阶段留空,P1 阶段实现恢复逻辑。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: ChannelRecovered 事件。
|
|
|
|
|
|
"""
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_transport_error(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理传输错误事件。
|
|
|
|
|
|
|
|
|
|
|
|
P0 阶段仅记录日志,P1 阶段可用于监控告警。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: TransportErrorOccurred 事件。
|
|
|
|
|
|
"""
|
|
|
|
|
|
await self._logger.warn(
|
|
|
|
|
|
"transport error occurred",
|
|
|
|
|
|
trace_id=event.trace_id,
|
|
|
|
|
|
channel_type=event.payload.get("channel_type"),
|
|
|
|
|
|
account_id=event.payload.get("account_id"),
|
|
|
|
|
|
error_category=event.payload.get("error_category"),
|
|
|
|
|
|
error_code=event.payload.get("error_code"),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def _on_config_changed(self, event: DomainEvent) -> None:
|
|
|
|
|
|
"""处理配置变更事件(FR-04 配置热更新)。
|
|
|
|
|
|
|
|
|
|
|
|
监听 ``transport.*`` 前缀的全局配置变更,按 hot/restart 模式应用:
|
|
|
|
|
|
- hot 模式(``stall_timeout_ms`` / ``backoff_schedule`` /
|
|
|
|
|
|
``backoff_jitter``):重新加载 TransportConfig 并更新 Worker 配置,
|
|
|
|
|
|
当前轮次结束后生效。
|
|
|
|
|
|
- restart 模式(``max_restart_attempts`` /
|
|
|
|
|
|
``graceful_shutdown_timeout_s``):记录警告日志,需手动重启
|
|
|
|
|
|
TransportManager 生效(P1 阶段不实现自动重启,避免账号连接中断)。
|
|
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
|
event: ConfigChanged / ConfigRollback 事件,payload 包含 key。
|
|
|
|
|
|
"""
|
|
|
|
|
|
key = event.payload.get("key", "")
|
|
|
|
|
|
if not key.startswith("transport."):
|
|
|
|
|
|
return
|
|
|
|
|
|
# 仅处理全局配置(target 为 None 或空),忽略账户级配置
|
|
|
|
|
|
target = event.payload.get("target")
|
|
|
|
|
|
if target:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
trace_id = event.trace_id or str(uuid.uuid4())
|
|
|
|
|
|
if key in _RESTART_REQUIRED_CONFIG_KEYS:
|
|
|
|
|
|
await self._logger.warn(
|
|
|
|
|
|
"transport config change requires restart to take effect",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
key=key,
|
|
|
|
|
|
event_type=event.event_type,
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 仅对 hot-reloadable 键触发重新加载,忽略未知 transport.* 键
|
|
|
|
|
|
if key not in _RELOADABLE_CONFIG_KEYS:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# hot 模式:重新加载配置并更新 Worker
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"transport config hot reload triggered",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
key=key,
|
|
|
|
|
|
event_type=event.event_type,
|
|
|
|
|
|
)
|
|
|
|
|
|
await self.reloadConfig()
|
|
|
|
|
|
|
|
|
|
|
|
async def reloadConfig(self) -> None:
|
|
|
|
|
|
"""重新加载传输配置并更新 Worker(FR-04 hot 模式)。
|
|
|
|
|
|
|
|
|
|
|
|
从 ``ConfigPort`` 读取最新 ``transport.*`` 配置,更新
|
|
|
|
|
|
``TransportManager._config`` 及两个 Worker 的 ``_config``。
|
|
|
|
|
|
Worker 在下一轮循环(退避等待、轮询间隔、心跳间隔)时自动应用新配置。
|
|
|
|
|
|
|
|
|
|
|
|
幂等:多次调用安全,仅更新配置不重启任务。
|
|
|
|
|
|
"""
|
|
|
|
|
|
trace_id = str(uuid.uuid4())
|
|
|
|
|
|
try:
|
|
|
|
|
|
new_config = await self._loadTransportConfig()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
await self._logger.error(
|
|
|
|
|
|
"transport config reload failed, keeping old config",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
error=str(exc),
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self._config = new_config
|
|
|
|
|
|
if self._puller_worker is not None:
|
|
|
|
|
|
self._puller_worker._config = new_config
|
|
|
|
|
|
if self._stream_worker is not None:
|
|
|
|
|
|
self._stream_worker._config = new_config
|
|
|
|
|
|
|
|
|
|
|
|
await self._logger.info(
|
|
|
|
|
|
"transport config reloaded",
|
|
|
|
|
|
trace_id=trace_id,
|
|
|
|
|
|
stall_timeout_ms=new_config.stall_timeout_ms,
|
|
|
|
|
|
backoff_schedule=list(new_config.backoff_schedule),
|
|
|
|
|
|
backoff_jitter=new_config.backoff_jitter,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
async def _loadTransportConfig(self) -> TransportConfig:
|
|
|
|
|
|
"""从 ConfigPort 加载 TransportConfig。
|
|
|
|
|
|
|
|
|
|
|
|
复用 BaseTransportWorker._loadConfig 的读取逻辑,集中配置加载
|
|
|
|
|
|
避免逻辑重复。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 委托任一 Worker 的 _loadConfig(两者配置相同),无 Worker 时
|
|
|
|
|
|
# 直接构造默认配置
|
|
|
|
|
|
if self._puller_worker is not None:
|
|
|
|
|
|
return await self._puller_worker._loadConfig()
|
|
|
|
|
|
if self._stream_worker is not None:
|
|
|
|
|
|
return await self._stream_worker._loadConfig()
|
|
|
|
|
|
return TransportConfig()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# transport.* 配置键的热更新模式分类(FR-04)
|
|
|
|
|
|
_RELOADABLE_CONFIG_KEYS: frozenset[str] = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
"transport.stall_timeout_ms",
|
|
|
|
|
|
"transport.backoff_schedule",
|
|
|
|
|
|
"transport.backoff_jitter",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
_RESTART_REQUIRED_CONFIG_KEYS: frozenset[str] = frozenset(
|
|
|
|
|
|
{
|
|
|
|
|
|
"transport.max_restart_attempts",
|
|
|
|
|
|
"transport.graceful_shutdown_timeout_s",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _AccountEventHandler(EventHandler):
|
|
|
|
|
|
"""账号事件处理器适配器。
|
|
|
|
|
|
|
|
|
|
|
|
将 EventHandler 接口适配到 TransportManager 的异步处理方法。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, handler: Callable[[DomainEvent], Awaitable[None]]) -> None:
|
|
|
|
|
|
self._handler = handler
|
|
|
|
|
|
|
|
|
|
|
|
async def handle(self, event: DomainEvent) -> None:
|
|
|
|
|
|
await self._handler(event)
|