WechatOnCloud/bridge/woc_bridge/ui/idem_cache.py

132 lines
3.9 KiB
Python
Raw Normal View History

"""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: 缓存存活秒数默认 3005 分钟
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_endLRU返回 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")