ForcePilot/backend/package/yuxi/channel/worker/outbox_retry.py
Kris c61d5f0163 feat: 完成通道服务多轮功能迭代
本次提交完成了一系列核心功能迭代与优化:
1.  新增并完善了多个领域模型与端口定义,补充了`__all__`导出规范
2.  优化了会话、绑定、出箱等模块的数据模型,修复了时间字段类型不一致问题
3.  新增了代理ID解析、缓存发布等接口,扩展了系统能力
4.  重构了去重中间件逻辑,优化了空内容校验规则
5.  新增了认证中间件的匿名访问支持,完善了鉴权流程
6.  优化了SSE连接管理,增加了单会话连接上限限制
7.  重构了消息日志与仓储相关代码,将数据类迁移至对应模型目录
8.  新增了重复绑定校验、绑定更新接口,完善了绑定服务逻辑
9.  优化了健康检查逻辑,新增了环境变量控制启动时间线展示
10. 重构了出箱重试工作线程,使用缓存端口替代直接redis操作,新增了消息处理标记逻辑
11. 完善了飞书、Web、钩子等通道的翻译器逻辑,补充了账户ID传递
12. 新增了多种自定义异常类型,优化了异常映射与错误处理流程
13. 完善了配置热重载逻辑,同步认证凭证与校验器配置
14. 重构了Redis缓存实现,增加了异常捕获与包装
2026-05-31 21:42:03 +08:00

100 lines
3.5 KiB
Python

from __future__ import annotations
import asyncio
import logging
from yuxi.channel.domain.model.message.dispatch_result import SendResult
from yuxi.channel.domain.port import CachePort
from yuxi.channel.domain.port.channel_adapter_port import ChannelAdapterPort
from yuxi.channel.domain.port.metrics_port import MetricsPort
from yuxi.channel.domain.repository.outbox_repository import OutboxRepositoryPort
logger = logging.getLogger(__name__)
_OUTBOX_NOTIFY_CHANNEL = "channel:outbox:notify"
class OutboxRetryWorker:
def __init__(
self,
outbox_repo: OutboxRepositoryPort,
adapters: dict[str, ChannelAdapterPort],
cache_port: CachePort | None = None,
*,
metrics: MetricsPort | None = None,
poll_interval: float = 5.0,
) -> None:
self._outbox = outbox_repo
self._adapters = adapters
self._cache = cache_port
self._metrics = metrics
self._poll_interval = poll_interval
self._running = False
self._task: asyncio.Task | None = None
self._notify_event: asyncio.Event = asyncio.Event()
async def start(self) -> None:
self._running = True
self._task = asyncio.create_task(self._loop())
logger.info("outbox retry worker started")
async def stop(self) -> None:
self._running = False
self._notify_event.set()
if self._task:
self._task.cancel()
await asyncio.gather(self._task, return_exceptions=True)
self._task = None
logger.info("outbox retry worker stopped")
async def _loop(self) -> None:
pubsub_task: asyncio.Task | None = None
try:
while self._running:
try:
await self._process_pending()
except asyncio.CancelledError:
break
except Exception as exc:
logger.error("outbox retry error: %s", exc)
try:
await asyncio.wait_for(self._notify_event.wait(), timeout=self._poll_interval)
self._notify_event.clear()
except TimeoutError:
pass
finally:
if pubsub_task:
pubsub_task.cancel()
try:
await asyncio.gather(pubsub_task, return_exceptions=True)
except Exception:
pass
async def _process_pending(self) -> None:
entries = await self._outbox.fetch_and_mark_pending()
for entry in entries:
adapter = self._adapters.get(entry.channel_type)
if not adapter:
continue
result: SendResult = await adapter.send_message(
entry.session_id,
entry.content,
channel_type=entry.channel_type,
metadata={"trace_id": entry.trace_id or ""},
)
if result.success:
await self._outbox.mark_sent(entry.id)
if self._metrics:
await self._metrics.record_outbox_retry_total(entry.channel_type, "success")
else:
retry_result = await self._outbox.mark_retrying(entry.id, last_error=result.error)
if retry_result and retry_result.status == "dead":
if self._metrics:
await self._metrics.record_outbox_retry_total(entry.channel_type, "dead")
elif self._metrics:
await self._metrics.record_outbox_retry_total(entry.channel_type, "retrying")