"""多渠道网关入站消息幂等去重。""" from yuxi.channel.constants import CHANNEL_PROCESSED_TTL_SECONDS, channel_processed_key from yuxi.services.run_queue_service import get_redis_client from yuxi.utils.logging_config import logger class MessageDeduper: """基于 Redis SET NX EX 的分布式消息去重器。 使用 ``channel_message_id`` 作为去重键,默认 5 分钟 TTL。 Redis 是入站去重的预设依赖;连接或命令失败时直接抛出异常, 以便运维及时发现,而不是通过内存回退掩盖问题。 """ def __init__(self, ttl_seconds: int = CHANNEL_PROCESSED_TTL_SECONDS) -> None: self.ttl = ttl_seconds async def is_processed(self, message_id: str | None) -> bool: """判断消息是否已处理;未处理则写入标记并返回 False。 空 ``channel_message_id`` 无法可靠去重,此时跳过去重并记录警告, 避免消息被静默丢弃;同一消息可能因此重复处理。 """ if not message_id: logger.warning("Empty channel_message_id, deduplication skipped") return False redis = await get_redis_client() # SET NX EX: 只有 key 不存在时才设置成功,返回 True。 # 返回 None 表示 key 已存在(消息已处理)。 return await redis.set(channel_processed_key(message_id), "1", nx=True, ex=self.ttl) is None async def is_already_processed(self, message_id: str | None) -> bool: """仅查询消息是否已处理,不设置去重标记。""" if not message_id: return False redis = await get_redis_client() return bool(await redis.exists(channel_processed_key(message_id))) async def mark_processed(self, message_id: str | None) -> None: """显式写入去重标记。""" if not message_id: return redis = await get_redis_client() await redis.setex(channel_processed_key(message_id), self.ttl, "1") async def clear_processed(self, message_id: str | None) -> None: """清除去重标记,用于处理失败后允许外部平台重试。""" if not message_id: return redis = await get_redis_client() await redis.delete(channel_processed_key(message_id)) async def safe_clear_processed(self, message_id: str | None) -> None: """安全清除去重标记,失败时仅记录 warning 不影响主流程响应。""" try: await self.clear_processed(message_id) except Exception: logger.warning("Failed to clear dedupe mark: %s", message_id)