ForcePilot/backend/package/yuxi/channels/adapters/dingding/dedup.py

90 lines
2.8 KiB
Python
Raw Normal View History

from __future__ import annotations
import time
from collections import OrderedDict
from yuxi.utils.logging_config import logger
_DEFAULT_MAX_ENTRIES = 10000
_DEFAULT_TTL_SECONDS = 86400
class DingDingDedupGuard:
def __init__(self, ttl: int = _DEFAULT_TTL_SECONDS, max_entries: int = _DEFAULT_MAX_ENTRIES):
self._ttl = ttl
self._max_entries = max_entries
self._pending: dict[str, float] = {}
self._committed: OrderedDict[str, float] = OrderedDict()
def _gc(self) -> None:
now = time.time()
stale = [k for k, ts in self._committed.items() if now - ts > self._ttl]
for k in stale:
del self._committed[k]
while len(self._committed) > self._max_entries:
self._committed.popitem(last=False)
def has_processed(self, message_id: str) -> bool:
if not message_id:
return False
self._gc()
return message_id in self._committed
def record_as_processed(self, message_id: str) -> None:
if not message_id:
return
self._gc()
self._pending.pop(message_id, None)
if message_id in self._committed:
self._committed.move_to_end(message_id)
self._committed[message_id] = time.time()
else:
if len(self._committed) >= self._max_entries:
self._committed.popitem(last=False)
self._committed[message_id] = time.time()
def claim(self, message_id: str) -> bool:
if not message_id:
return True
self._gc()
if message_id in self._committed:
logger.debug(f"[DingDing] Dedup: duplicate message rejected {message_id}")
return False
if message_id in self._pending:
logger.debug(f"[DingDing] Dedup: already pending {message_id}")
return False
self._pending[message_id] = time.time()
return True
def commit(self, message_id: str) -> None:
if not message_id:
return
self._pending.pop(message_id, None)
self._committed[message_id] = time.time()
self._gc()
def release(self, message_id: str) -> None:
if not message_id:
return
self._pending.pop(message_id, None)
def finalize_processing(self, message_id: str) -> None:
self.commit(message_id)
def check_and_mark(self, message_id: str) -> bool:
if self.has_processed(message_id):
return True
self.record_as_processed(message_id)
return False
def stats(self) -> dict[str, int]:
self._gc()
return {
"committed": len(self._committed),
"pending": len(self._pending),
}
def clear(self) -> None:
self._pending.clear()
self._committed.clear()