新增了插件相关的完整领域模型、应用服务、基础设施实现,包括: 1. 插件状态、注册模式、来源等基础枚举和数据结构 2. 插件清单解析、发现、加载工具类 3. 插件注册表领域服务和内存存储实现 4. 插件相关的命令、查询、事件定义 5. 插件REST API接口和DTO映射 6. 集成了原有通道适配器到插件系统 7. 新增内置插件注册和自动发现能力
131 lines
4.6 KiB
Python
131 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
|
|
import redis.asyncio as aioredis
|
|
|
|
from yuxi.channel.domain.model.message.dispatch_result import SendResult
|
|
from yuxi.channel.domain.model.outbox.outbox_status import OutboxStatus
|
|
from yuxi.channel.domain.model.plugin_registry.plugin_registry import PluginRegistry
|
|
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],
|
|
redis: aioredis.Redis | None = None,
|
|
*,
|
|
metrics: MetricsPort | None = None,
|
|
poll_interval: float = 5.0,
|
|
registry: PluginRegistry | None = None,
|
|
) -> None:
|
|
self._outbox = outbox_repo
|
|
self._adapters = adapters
|
|
self._registry = registry
|
|
self._redis = redis
|
|
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
|
|
if self._redis:
|
|
pubsub_task = asyncio.create_task(self._listen_notifications())
|
|
|
|
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 _listen_notifications(self) -> None:
|
|
if not self._redis:
|
|
return
|
|
pubsub = self._redis.pubsub()
|
|
try:
|
|
await pubsub.subscribe(_OUTBOX_NOTIFY_CHANNEL)
|
|
async for message in pubsub.listen():
|
|
if message["type"] == "message":
|
|
self._notify_event.set()
|
|
except asyncio.CancelledError:
|
|
pass
|
|
except Exception:
|
|
logger.warning("outbox pubsub listener failed, falling back to polling")
|
|
finally:
|
|
try:
|
|
await pubsub.unsubscribe(_OUTBOX_NOTIFY_CHANNEL)
|
|
await pubsub.aclose()
|
|
except Exception:
|
|
pass
|
|
|
|
async def _process_pending(self) -> None:
|
|
entries = await self._outbox.fetch_and_lock()
|
|
for entry in entries:
|
|
adapter = (
|
|
self._registry.get_adapter(entry.channel_type)
|
|
if self._registry
|
|
else 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:
|
|
updated = await self._outbox.mark_retrying(entry.id, last_error=result.error)
|
|
if updated and updated.status == OutboxStatus.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")
|