base_agent/uas/comm/adapters/redis_adapter.py

142 lines
5.4 KiB
Python
Raw Normal View History

2026-06-18 03:28:14 +00:00
# -*- coding: utf-8 -*-
"""
Redis 适配器
基于 redis-py >= 4.x 的异步客户端
支持Pub/SubStream持久化Key/Value 消息存储
"""
import asyncio
import json
import logging
from typing import Optional
import redis.asyncio as aioredis
from uas.comm.base import CommBaseAdapter, ConnectionConfig, MessageHandler
from uas.comm.message import Message
logger = logging.getLogger(__name__)
class RedisAdapterComm(CommBaseAdapter):
"""
Redis 通信适配器
URL 格式: redis://[:password@]host[:port][/db][?options]
示例: redis://:secret@127.0.0.1:6379/0?socket_timeout=5
"""
def __init__(self, config: ConnectionConfig):
super().__init__(config)
self._client: Optional[aioredis.Redis] = None
self._pubsub: Optional[aioredis.client.PubSub] = None
self._listen_task: Optional[asyncio.Task] = None
# ── 生命周期 ──────────────────────────────────────────────────
async def connect(self) -> None:
opts = self.config.options
db = int(self.config.database) if self.config.database.isdigit() else 0
self._client = aioredis.Redis(
host = self.config.host,
port = self.config.port,
password = self.config.password or None,
db = db,
socket_timeout = float(opts.get("socket_timeout", 5)),
decode_responses = True,
)
# 连通性测试
await self._client.ping()
self._pubsub = self._client.pubsub()
self._running = True
self.logger.info(f"✅ Redis 已连接: {self.config.host}:{self.config.port} db={db}")
async def disconnect(self) -> None:
self._running = False
if self._listen_task:
self._listen_task.cancel()
try:
await self._listen_task
except asyncio.CancelledError:
pass
if self._pubsub:
await self._pubsub.close()
if self._client:
await self._client.aclose()
self.logger.info("🔌 Redis 连接已关闭")
# ── 核心功能 ──────────────────────────────────────────────────
async def publish(self, message: Message) -> bool:
if not self._client:
raise RuntimeError("Redis 未连接")
try:
payload = json.dumps(message.to_dict(), ensure_ascii=False)
receivers = await self._client.publish(message.topic, payload)
self.logger.debug(f"📤 发布 topic={message.topic!r} receivers={receivers}")
return True
except Exception as e:
self.logger.error(f"发布失败: {e}")
return False
async def subscribe(self, topic: str, handler: MessageHandler) -> None:
if not self._pubsub:
raise RuntimeError("Redis 未连接")
self._register_handler(topic, handler)
# 支持通配符订阅
if "*" in topic or "?" in topic:
await self._pubsub.psubscribe(topic)
else:
await self._pubsub.subscribe(topic)
# 启动监听循环(幂等)
if self._listen_task is None or self._listen_task.done():
self._listen_task = asyncio.create_task(self._listen_loop())
self.logger.info(f"📥 订阅 Redis topic: {topic!r}")
async def unsubscribe(self, topic: str) -> None:
if self._pubsub:
if "*" in topic or "?" in topic:
await self._pubsub.punsubscribe(topic)
else:
await self._pubsub.unsubscribe(topic)
self._handlers.pop(topic, None)
self.logger.info(f"🚫 取消订阅: {topic!r}")
# ── Stream 持久化发布(可选)──────────────────────────────────
async def publish_stream(self, message: Message, maxlen: int = 1000) -> str:
"""使用 Redis Stream 发布(消息持久化,支持消费组)"""
stream_id = await self._client.xadd(
message.topic,
{"data": json.dumps(message.to_dict(), ensure_ascii=False)},
maxlen=maxlen,
)
self.logger.debug(f"📤 Stream 发布 id={stream_id}")
return stream_id
# ── 内部监听循环 ──────────────────────────────────────────────
async def _listen_loop(self):
self.logger.info("🔄 Redis 监听循环启动")
try:
async for raw in self._pubsub.listen():
if raw["type"] not in ("message", "pmessage"):
continue
try:
data = json.loads(raw["data"])
message = Message.from_dict(data)
await self._dispatch(message)
except Exception as e:
self.logger.error(f"消息解析异常: {e}", exc_info=True)
except asyncio.CancelledError:
self.logger.info("🛑 Redis 监听循环已停止")
async def health_check(self) -> bool:
try:
return bool(await self._client.ping())
except Exception:
return False