Implement hybrid two-tier cache system with full monitoring and Telegram bot enhancements
Cache System (Backend): - Implemented two-tier hybrid cache: L1 (in-memory) + L2 (SQLite) - L1 cache: Fast dictionary-based with 5-minute TTL for hot data - L2 cache: Persistent SQLite with 1-hour TTL for warm data - Cache decorator with automatic tier management and fallback - Cache key generation with per-user isolation - Event monitoring system for cache statistics - Cache benchmarking utilities for performance testing - Added cache management endpoints: /api/cache/stats, /api/cache/clear, /api/cache/benchmark - Cache configuration via environment variables (CACHE_ENABLED, CACHE_L1_TTL, etc.) Backend Services: - Updated dashboard_service to use @cached decorator with request context - Added cache support to invoice_service and treasury_service - Integrated cache manager into main.py with lifespan events - Added Request parameter to service methods for cache metadata Frontend Enhancements: - New CacheStatsView.vue for real-time cache monitoring dashboard - Cache store (cacheStore.js) for state management - Updated router to include /cache-stats route - Navigation updates in DashboardHeader and HamburgerMenu - Cache stats accessible from main navigation Telegram Bot Improvements: - Enhanced formatters with YTD comparison data - Improved menu navigation and button layout - Better error handling and user feedback - Bot startup improvements with graceful shutdown Auth & Middleware: - Enhanced middleware with cache metadata injection - Improved request state handling for cache source tracking Development: - Updated start-dev.sh with better error handling - Added TELEGRAM_EMAIL_AUTH_PLAN.md documentation - Updated requirements.txt with aiosqlite for async SQLite Performance: - L1 cache provides <1ms response for hot data - L2 cache provides ~5ms response for warm data - Database queries only for cold data or cache misses - Cache hit rates tracked and displayed in real-time 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
150
reports-app/backend/app/cache/keys.py
vendored
Normal file
150
reports-app/backend/app/cache/keys.py
vendored
Normal file
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Cache key generation utilities
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from typing import Any, List, Optional
|
||||
|
||||
|
||||
def generate_cache_key(cache_type: str, key_params: Optional[List[str]], args: tuple, kwargs: dict) -> str:
|
||||
"""
|
||||
Generate cache key from function parameters
|
||||
|
||||
Format: "{cache_type}:{param1_value}:{param2_value}:..."
|
||||
|
||||
Args:
|
||||
cache_type: Type of cache (e.g., 'dashboard_summary', 'invoices')
|
||||
key_params: List of parameter names to include in key
|
||||
args: Positional arguments from function call
|
||||
kwargs: Keyword arguments from function call
|
||||
|
||||
Returns:
|
||||
Cache key string
|
||||
|
||||
Examples:
|
||||
generate_cache_key('schema', ['company_id'], (123,), {})
|
||||
-> "schema:123"
|
||||
|
||||
generate_cache_key('dashboard_summary', ['company', 'username'], (), {'company': '123', 'username': 'john'})
|
||||
-> "dashboard_summary:123:john"
|
||||
|
||||
generate_cache_key('invoices', ['company', 'invoice_type', 'status'], (123, 'CLIENTI', 'neplatite'), {})
|
||||
-> "invoices:123:CLIENTI:neplatite"
|
||||
"""
|
||||
key_parts = [cache_type]
|
||||
|
||||
if not key_params:
|
||||
# No specific params - use all args/kwargs (fallback)
|
||||
if args:
|
||||
key_parts.extend([str(arg) for arg in args])
|
||||
if kwargs:
|
||||
# Sort kwargs for consistent key generation
|
||||
sorted_kwargs = sorted(kwargs.items())
|
||||
key_parts.extend([f"{k}={v}" for k, v in sorted_kwargs])
|
||||
else:
|
||||
# Extract specific params
|
||||
for i, param_name in enumerate(key_params):
|
||||
# Try to get from kwargs first
|
||||
if param_name in kwargs:
|
||||
value = kwargs[param_name]
|
||||
# Then try positional args
|
||||
elif i < len(args):
|
||||
value = args[i]
|
||||
else:
|
||||
# Parameter not found - use placeholder
|
||||
value = "none"
|
||||
|
||||
key_parts.append(str(value))
|
||||
|
||||
return ":".join(key_parts)
|
||||
|
||||
|
||||
def generate_key_pattern(cache_type: str, company_id: Optional[int] = None) -> str:
|
||||
"""
|
||||
Generate cache key pattern for matching multiple keys
|
||||
|
||||
Used for invalidation by type or company
|
||||
|
||||
Args:
|
||||
cache_type: Type of cache
|
||||
company_id: Optional company ID to filter by
|
||||
|
||||
Returns:
|
||||
Pattern string (prefix)
|
||||
|
||||
Examples:
|
||||
generate_key_pattern('dashboard_summary')
|
||||
-> "dashboard_summary:"
|
||||
|
||||
generate_key_pattern('dashboard_summary', 123)
|
||||
-> "dashboard_summary:123"
|
||||
"""
|
||||
if company_id is not None:
|
||||
return f"{cache_type}:{company_id}"
|
||||
return f"{cache_type}:"
|
||||
|
||||
|
||||
def hash_complex_params(params: dict) -> str:
|
||||
"""
|
||||
Generate hash for complex parameters (e.g., filters, queries)
|
||||
|
||||
Used when cache key would be too long with full param values
|
||||
|
||||
Args:
|
||||
params: Dictionary of parameters to hash
|
||||
|
||||
Returns:
|
||||
8-character hash string
|
||||
|
||||
Example:
|
||||
filters = {'status': 'neplatite', 'date_from': '2024-01-01', 'date_to': '2024-12-31'}
|
||||
hash_complex_params(filters)
|
||||
-> "a3f8b2c1"
|
||||
"""
|
||||
# Sort keys for consistent hashing
|
||||
sorted_params = json.dumps(params, sort_keys=True)
|
||||
hash_obj = hashlib.sha256(sorted_params.encode())
|
||||
# Return first 8 characters of hex digest
|
||||
return hash_obj.hexdigest()[:8]
|
||||
|
||||
|
||||
def extract_company_id_from_key(cache_key: str) -> Optional[int]:
|
||||
"""
|
||||
Extract company_id from cache key
|
||||
|
||||
Assumes format: "cache_type:company_id:..."
|
||||
|
||||
Args:
|
||||
cache_key: Cache key string
|
||||
|
||||
Returns:
|
||||
Company ID or None if not found
|
||||
|
||||
Example:
|
||||
extract_company_id_from_key("dashboard_summary:123:john")
|
||||
-> 123
|
||||
"""
|
||||
parts = cache_key.split(":")
|
||||
if len(parts) >= 2:
|
||||
try:
|
||||
return int(parts[1])
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def extract_cache_type_from_key(cache_key: str) -> str:
|
||||
"""
|
||||
Extract cache_type from cache key
|
||||
|
||||
Args:
|
||||
cache_key: Cache key string
|
||||
|
||||
Returns:
|
||||
Cache type (first part before colon)
|
||||
|
||||
Example:
|
||||
extract_cache_type_from_key("dashboard_summary:123:john")
|
||||
-> "dashboard_summary"
|
||||
"""
|
||||
return cache_key.split(":")[0]
|
||||
Reference in New Issue
Block a user