82 lines
2.4 KiB
Python
82 lines
2.4 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
import os
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
PAY_DEDUP_TTL_SECONDS = 259200
|
||
|
|
|
||
|
|
|
||
|
|
class PayNotifyDeduplicator:
|
||
|
|
def __init__(self, redis_client):
|
||
|
|
self._redis = redis_client
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _key(notification_id: str) -> str:
|
||
|
|
return f"wp:dedup:{notification_id}"
|
||
|
|
|
||
|
|
async def try_claim(self, notification_id: str) -> bool:
|
||
|
|
key = self._key(notification_id)
|
||
|
|
try:
|
||
|
|
result = await self._redis.set(key, "1", nx=True, ex=PAY_DEDUP_TTL_SECONDS)
|
||
|
|
return bool(result)
|
||
|
|
except Exception:
|
||
|
|
logger.exception("PayNotifyDeduplicator Redis error, allowing through")
|
||
|
|
return True
|
||
|
|
|
||
|
|
async def get_status(self, notification_id: str) -> str | None:
|
||
|
|
key = self._key(notification_id)
|
||
|
|
try:
|
||
|
|
val = await self._redis.get(key)
|
||
|
|
return val.decode("utf-8") if val else None
|
||
|
|
except Exception:
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
class InMemoryPayDeduplicator:
|
||
|
|
def __init__(self, max_size: int = 2000):
|
||
|
|
from collections import OrderedDict
|
||
|
|
|
||
|
|
self._store: OrderedDict[str, float] = OrderedDict()
|
||
|
|
self._max_size = max_size
|
||
|
|
|
||
|
|
async def try_claim(self, notification_id: str) -> bool:
|
||
|
|
import time
|
||
|
|
|
||
|
|
now = time.monotonic()
|
||
|
|
|
||
|
|
if notification_id in self._store:
|
||
|
|
return False
|
||
|
|
|
||
|
|
self._store[notification_id] = now
|
||
|
|
while len(self._store) > self._max_size:
|
||
|
|
self._store.popitem(last=False)
|
||
|
|
return True
|
||
|
|
|
||
|
|
|
||
|
|
_DEDUP_INSTANCE = None
|
||
|
|
|
||
|
|
|
||
|
|
def get_pay_deduplicator():
|
||
|
|
global _DEDUP_INSTANCE
|
||
|
|
if _DEDUP_INSTANCE is not None:
|
||
|
|
return _DEDUP_INSTANCE
|
||
|
|
|
||
|
|
redis_url = os.environ.get("YUXI_REDIS_URL", os.environ.get("REDIS_URL", ""))
|
||
|
|
|
||
|
|
if redis_url:
|
||
|
|
try:
|
||
|
|
import redis.asyncio as aioredis
|
||
|
|
|
||
|
|
client = aioredis.from_url(redis_url, decode_responses=False)
|
||
|
|
_DEDUP_INSTANCE = PayNotifyDeduplicator(client)
|
||
|
|
logger.info("PayNotifyDeduplicator: using Redis backend (TTL=%ds)", PAY_DEDUP_TTL_SECONDS)
|
||
|
|
return _DEDUP_INSTANCE
|
||
|
|
except Exception:
|
||
|
|
logger.warning("PayNotifyDeduplicator: Redis unavailable, falling back to in-memory")
|
||
|
|
|
||
|
|
_DEDUP_INSTANCE = InMemoryPayDeduplicator()
|
||
|
|
logger.warning("PayNotifyDeduplicator: using in-memory backend (NOT production-ready)")
|
||
|
|
return _DEDUP_INSTANCE
|