64 lines
2.6 KiB
Python
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)
|