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()