132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
|
|
"""UI 自动化幂等缓存:防止短时间内重复发送相同消息。
|
|||
|
|
|
|||
|
|
key 由 (flow_name, to_wxid, content, client_request_id) 经 SHA256 完整哈希生成,
|
|||
|
|
5 分钟 TTL,最多 1000 条。命中时返回缓存的 SendResult,避免重复发送。
|
|||
|
|
|
|||
|
|
LRU 淘汰 + TTL 过期双策略,保证内存占用有上限且旧数据自动失效。
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import hashlib
|
|||
|
|
import logging
|
|||
|
|
import time
|
|||
|
|
from collections import OrderedDict
|
|||
|
|
from typing import Any, Optional
|
|||
|
|
|
|||
|
|
logger = logging.getLogger("woc-bridge")
|
|||
|
|
|
|||
|
|
|
|||
|
|
class IdemCache:
|
|||
|
|
"""幂等缓存:LRU + TTL 双策略。
|
|||
|
|
|
|||
|
|
Attributes:
|
|||
|
|
ttl: 缓存存活秒数,默认 300(5 分钟)
|
|||
|
|
max_size: 最大缓存条目数,默认 1000
|
|||
|
|
_cache: OrderedDict[key_str, (value, expire_at)]
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, ttl: float = 300.0, max_size: int = 1000) -> None:
|
|||
|
|
"""初始化幂等缓存。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
ttl: 缓存存活秒数
|
|||
|
|
max_size: 最大缓存条目数
|
|||
|
|
"""
|
|||
|
|
self.ttl = ttl
|
|||
|
|
self.max_size = max_size
|
|||
|
|
self._cache: OrderedDict[str, tuple[Any, float]] = OrderedDict()
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _make_key(
|
|||
|
|
flow_name: str,
|
|||
|
|
to_wxid: str,
|
|||
|
|
content: str,
|
|||
|
|
client_request_id: str = "",
|
|||
|
|
) -> str:
|
|||
|
|
"""生成缓存 key:完整 SHA256(不截断)。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
flow_name: 流程名(如 "send_text")
|
|||
|
|
to_wxid: 目标 wxid
|
|||
|
|
content: 消息内容
|
|||
|
|
client_request_id: 客户端请求 ID(可选,用于显式幂等)
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
64 字符 hex SHA256 摘要
|
|||
|
|
"""
|
|||
|
|
raw = f"{flow_name}|{to_wxid}|{content}|{client_request_id}"
|
|||
|
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|||
|
|
|
|||
|
|
def get(
|
|||
|
|
self,
|
|||
|
|
flow_name: str,
|
|||
|
|
to_wxid: str,
|
|||
|
|
content: str,
|
|||
|
|
client_request_id: str = "",
|
|||
|
|
) -> Optional[Any]:
|
|||
|
|
"""查询缓存。
|
|||
|
|
|
|||
|
|
命中且未过期:move_to_end(LRU),返回 value。
|
|||
|
|
命中但已过期:删除,返回 None。
|
|||
|
|
未命中:返回 None。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
flow_name: 流程名
|
|||
|
|
to_wxid: 目标 wxid
|
|||
|
|
content: 消息内容
|
|||
|
|
client_request_id: 客户端请求 ID
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
缓存的 value 或 None
|
|||
|
|
"""
|
|||
|
|
key = self._make_key(flow_name, to_wxid, content, client_request_id)
|
|||
|
|
entry = self._cache.get(key)
|
|||
|
|
if entry is None:
|
|||
|
|
return None
|
|||
|
|
value, expire_at = entry
|
|||
|
|
if time.monotonic() >= expire_at:
|
|||
|
|
# TTL 过期,删除
|
|||
|
|
del self._cache[key]
|
|||
|
|
logger.debug("[idem] key expired: %s", key[:12])
|
|||
|
|
return None
|
|||
|
|
# LRU:移到末尾
|
|||
|
|
self._cache.move_to_end(key)
|
|||
|
|
logger.debug("[idem] hit: %s", key[:12])
|
|||
|
|
return value
|
|||
|
|
|
|||
|
|
def set(
|
|||
|
|
self,
|
|||
|
|
flow_name: str,
|
|||
|
|
to_wxid: str,
|
|||
|
|
content: str,
|
|||
|
|
value: Any,
|
|||
|
|
client_request_id: str = "",
|
|||
|
|
) -> None:
|
|||
|
|
"""写入缓存。
|
|||
|
|
|
|||
|
|
若 key 已存在则更新 value 并 move_to_end。
|
|||
|
|
超限 max_size 时 popitem(last=False) 删最旧。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
flow_name: 流程名
|
|||
|
|
to_wxid: 目标 wxid
|
|||
|
|
content: 消息内容
|
|||
|
|
value: 缓存的值(通常为 FlowResult)
|
|||
|
|
client_request_id: 客户端请求 ID
|
|||
|
|
"""
|
|||
|
|
key = self._make_key(flow_name, to_wxid, content, client_request_id)
|
|||
|
|
expire_at = time.monotonic() + self.ttl
|
|||
|
|
if key in self._cache:
|
|||
|
|
self._cache.move_to_end(key)
|
|||
|
|
self._cache[key] = (value, expire_at)
|
|||
|
|
# LRU 淘汰
|
|||
|
|
while len(self._cache) > self.max_size:
|
|||
|
|
self._cache.popitem(last=False)
|
|||
|
|
logger.debug("[idem] set: %s (size=%d)", key[:12], len(self._cache))
|
|||
|
|
|
|||
|
|
def clear(self) -> None:
|
|||
|
|
"""清空缓存。"""
|
|||
|
|
self._cache.clear()
|
|||
|
|
logger.info("[idem] cache cleared")
|