base_agent/uas/comm/adapters/redis_adapter.py

142 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
Redis 适配器
基于 redis-py >= 4.x 的异步客户端
支持Pub/Sub、Stream持久化、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