base_agent/uas/comm/adapters/kafka_adapter.py

139 lines
5.4 KiB
Python

# -*- coding: utf-8 -*-
"""
Kafka 适配器
基于 aiokafka 异步客户端
支持:生产者/消费者、消费组、分区、偏移量管理
"""
import asyncio
import json
import logging
from typing import Optional
from aiokafka import AIOKafkaProducer, AIOKafkaConsumer
from aiokafka.errors import KafkaError
from uas.comm.base import CommBaseAdapter, ConnectionConfig, MessageHandler
from uas.comm.message import Message
logger = logging.getLogger(__name__)
class KafkaAdapterComm(CommBaseAdapter):
"""
Kafka 通信适配器
URL 格式: kafka://[user:pass@]host[:port][/group_id][?options]
示例: kafka://broker:9092/my-group?auto_offset_reset=earliest&acks=all
"""
def __init__(self, config: ConnectionConfig):
super().__init__(config)
self._producer: Optional[AIOKafkaProducer] = None
self._consumers: dict[str, AIOKafkaConsumer] = {}
self._consume_tasks: dict[str, asyncio.Task] = {}
@property
def _bootstrap_servers(self) -> str:
return f"{self.config.host}:{self.config.port}"
@property
def _group_id(self) -> str:
return self.config.database or "default-group"
# ── 生命周期 ──────────────────────────────────────────────────
async def connect(self) -> None:
opts = self.config.options
self._producer = AIOKafkaProducer(
bootstrap_servers = self._bootstrap_servers,
acks = opts.get("acks", "all"),
compression_type = opts.get("compression_type", None),
value_serializer = lambda v: json.dumps(v, ensure_ascii=False).encode(),
)
await self._producer.start()
self._running = True
self.logger.info(f"✅ Kafka Producer 已连接: {self._bootstrap_servers}")
async def disconnect(self) -> None:
self._running = False
# 停止所有消费任务
for topic, task in self._consume_tasks.items():
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
# 停止所有消费者
for consumer in self._consumers.values():
await consumer.stop()
# 停止生产者
if self._producer:
await self._producer.stop()
self.logger.info("🔌 Kafka 连接已关闭")
# ── 核心功能 ──────────────────────────────────────────────────
async def publish(self, message: Message) -> bool:
if not self._producer:
raise RuntimeError("Kafka 未连接")
try:
await self._producer.send_and_wait(
message.topic,
value = message.to_dict(),
headers = [(k, v.encode()) for k, v in message.headers.items()],
)
self.logger.debug(f"📤 Kafka 发布 topic={message.topic!r}")
return True
except KafkaError as e:
self.logger.error(f"Kafka 发布失败: {e}")
return False
async def subscribe(self, topic: str, handler: MessageHandler) -> None:
opts = self.config.options
self._register_handler(topic, handler)
if topic not in self._consumers:
consumer = AIOKafkaConsumer(
topic,
bootstrap_servers = self._bootstrap_servers,
group_id = self._group_id,
auto_offset_reset = opts.get("auto_offset_reset", "latest"),
enable_auto_commit = opts.get("enable_auto_commit", "true") == "true",
value_deserializer = lambda v: json.loads(v.decode()),
)
await consumer.start()
self._consumers[topic] = consumer
self._consume_tasks[topic] = asyncio.create_task(
self._consume_loop(topic, consumer)
)
self.logger.info(f"📥 订阅 Kafka topic: {topic!r} group={self._group_id}")
async def unsubscribe(self, topic: str) -> None:
if topic in self._consume_tasks:
self._consume_tasks[topic].cancel()
del self._consume_tasks[topic]
if topic in self._consumers:
await self._consumers[topic].stop()
del self._consumers[topic]
self._handlers.pop(topic, None)
self.logger.info(f"🚫 取消订阅 Kafka topic: {topic!r}")
# ── 内部消费循环 ──────────────────────────────────────────────
async def _consume_loop(self, topic: str, consumer: AIOKafkaConsumer):
self.logger.info(f"🔄 Kafka 消费循环启动: {topic!r}")
try:
async for record in consumer:
try:
message = Message.from_dict(record.value)
await self._dispatch(message)
except Exception as e:
self.logger.error(f"消息处理异常: {e}", exc_info=True)
except asyncio.CancelledError:
self.logger.info(f"🛑 Kafka 消费循环停止: {topic!r}")
async def health_check(self) -> bool:
try:
return self._producer is not None and self._running
except Exception:
return False