ForcePilot/backend/package/yuxi/channel/message/dedupe.py
Kris bab30f2715
Some checks failed
Deploy VitePress site to Pages / build (push) Has been cancelled
Ruff Format Check / Ruff Format & Lint (push) Has been cancelled
Deploy VitePress site to Pages / Deploy (push) Has been cancelled
feat:0715
2026-07-15 12:30:58 +08:00

64 lines
2.6 KiB
Python

"""多渠道网关入站消息幂等去重。"""
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)