rmi-backend/app/caching_shield/history_depth.py
opencode c762564d40 style(rmi-backend): complete lint cleanup — 1175→0 ruff errors
- Fix 71 invalid-syntax files (class-body newline-broken assignments)
- Add from/None chain to 307 B904 raise-without-from sites
- Add B008 ignore to ruff.toml (already in pyproject.toml)
- Noqa F401 on __init__.py re-exports (137 sites)
- Noqa E402 on deferred imports (63 sites)
- Bulk-add stdlib/FastAPI/project imports for F821 (127 sites)
- Replace ×→x, –→-, …→... in docstrings (4093 chars)
- Manual refactor of 5 SIM103/SIM116 patterns

Tests: 791 passed (66 deselected due to pre-existing Redis issues in test_rag.py)
Co-authored-by: opencode <opencode@rugmunch.io>
2026-07-06 15:43:20 +02:00

138 lines
4.7 KiB
Python

"""
Aggressive Caching Shield - Historical Depth Controller
Controls how far back transaction history queries go. Free tier RPC
endpoints often rate-limit or block deep history queries. This module
caps default queries to shallow depth and gates deep queries.
Strategy:
- DEFAULT_DEPTH: 20 signatures (fast scan - enough to detect recent activity)
- MAX_DEPTH: 100 signatures (deep scan - on-demand only, user clicks button)
- MAX_PAGINATED: 200 (absolute maximum across all pages)
- Deep queries are cached longer (5min vs 30s) since historical data rarely changes
- Per-address cooldown: deep scan limited to once per 5 min per address
- Before/signature pagination uses the "before" parameter (not offset-based)
Cache integration:
- Shallow queries (limit <= 20): TTL 30s (default getSignaturesForAddress)
- Deep queries (limit > 20): TTL 300s (5min, historical data is static)
- Pagination queries: TTL 180s
"""
import logging
import time
from collections import OrderedDict
logger = logging.getLogger("history_depth")
# Default query depth for fast surface-level scans
DEFAULT_DEPTH = 20
# Maximum depth for a single query (prevents abuse)
MAX_DEPTH = 100
# Absolute maximum across all pagination
MAX_PAGINATED = 200
# Cooldown between deep scans per address (seconds)
DEEP_SCAN_COOLDOWN = 300 # 5 minutes
# How many addresses to track for cooldown
COOLDOWN_TRACK_SIZE = 500
class HistoryDepthController:
"""Controls transaction history query depth and deep-scan gating.
Usage:
ctrl = HistoryDepthController()
limit = ctrl.clamp_limit(requested_limit=50, is_deep_scan=False)
# -> returns 50 (within bounds, allowed)
can_proceed, wait = ctrl.check_deep_cooldown("SoL...")
if not can_proceed:
raise DeepScanThrottled(f"Deep scan cooldown: {wait:.0f}s remaining")
"""
def __init__(self):
# Per-address deep scan cooldown: address -> last_deep_scan_ts
self._deep_cooldowns: OrderedDict = OrderedDict()
def clamp_limit(
self,
requested: int = DEFAULT_DEPTH,
is_deep_scan: bool = False,
is_paginated: bool = False,
) -> int:
"""Clamp a requested query limit to allowed bounds.
Args:
requested: The limit the caller wants
is_deep_scan: True if this is an explicit deep-scan request (user clicked button)
is_paginated: True if this is a continuation (before/after signature pagination)
Returns:
Clamped limit value
"""
if is_deep_scan:
return min(requested, MAX_DEPTH)
elif is_paginated:
return min(requested, MAX_PAGINATED)
else:
# Default shallow scan
return min(requested, DEFAULT_DEPTH)
def check_deep_cooldown(self, address: str) -> tuple[bool, float]:
"""Check if a deep scan is allowed for this address.
Returns:
(allowed, wait_seconds) - if not allowed, wait_seconds is how long to wait
"""
now = time.monotonic()
last = self._deep_cooldowns.get(address)
if last is not None:
elapsed = now - last
if elapsed < DEEP_SCAN_COOLDOWN:
wait = DEEP_SCAN_COOLDOWN - elapsed
return (False, wait)
return (True, 0.0)
def record_deep_scan(self, address: str):
"""Record that a deep scan was performed for this address."""
self._deep_cooldowns[address] = time.monotonic()
# Evict oldest if over capacity
while len(self._deep_cooldowns) > COOLDOWN_TRACK_SIZE:
self._deep_cooldowns.popitem(last=False)
def get_deep_cache_ttl(self, depth: int) -> int:
"""Get appropriate cache TTL based on query depth."""
if depth > DEFAULT_DEPTH:
return 300 # 5 min for deep queries
elif depth > 10:
return 60 # 1 min for medium queries
else:
return 30 # 30s for shallow queries
def stats(self) -> dict:
"""Return controller statistics."""
return {
"addresses_in_cooldown": len(self._deep_cooldowns),
"default_depth": DEFAULT_DEPTH,
"max_depth": MAX_DEPTH,
"max_paginated": MAX_PAGINATED,
"deep_scan_cooldown_s": DEEP_SCAN_COOLDOWN,
}
# ── Singleton ──────────────────────────────────────────────────────────────
_controller: HistoryDepthController | None = None
def get_history_controller() -> HistoryDepthController:
global _controller
if _controller is None:
_controller = HistoryDepthController()
return _controller