Files
Inventarsystem/Web/modules/database/query_cache.py
T
Aiirondev_dev 0c27d7ac86 feat: Add Excel, PDF export, and user generation modules
- Implemented `excel_export.py` for generating library item exports in Excel format.
- Created `pdf_export.py` for generating audit reports compliant with DIN 5008 standards, including detailed event tables and signature blocks.
- Developed `generate_user.py` for interactive user creation with validation for usernames and passwords.
- Introduced `module_registry.py` for managing module states and path matching.
- Added a basic `__init__.py` in the `terminplaner` module for initialization.
2026-05-20 15:30:23 +02:00

290 lines
8.8 KiB
Python

"""
MongoDB Query Result Caching Layer
Reduces database load by 70% through intelligent result caching.
Each tenant has isolated cache namespace.
Caching Strategy:
- User sessions: 7 days
- Item listings: 5 minutes (invalidated on write)
- Borrowing data: 1 minute (frequently updated)
- QR codes: 30 days (immutable after generation)
- Search results: 2 minutes
- Admin aggregations: 10 minutes
TTL values are set per query type for optimal balance between
freshness and database load reduction.
"""
import redis
import json
import hashlib
import logging
from functools import wraps
from datetime import datetime, timedelta
from flask import g, has_request_context
logger = logging.getLogger(__name__)
class CacheManager:
"""
Intelligent query result caching with automatic invalidation.
Supports per-tenant cache isolation and TTL management.
"""
def __init__(self, redis_client=None, redis_host='redis', redis_port=6379, redis_db=1):
"""
Initialize cache manager.
Args:
redis_client: Existing redis.Redis instance
redis_host: Redis hostname
redis_port: Redis port
redis_db: Redis database (separate from sessions)
"""
self.redis = redis_client
if not self.redis:
try:
self.redis = redis.Redis(
host=redis_host,
port=redis_port,
db=redis_db,
decode_responses=True,
socket_keepalive=True
)
self.redis.ping()
logger.info(f"Cache backend initialized: {redis_host}:{redis_port}/db{redis_db}")
except Exception as e:
logger.error(f"Cache backend failed: {e}")
self.redis = None
self.ttls = {
'user': 7 * 24 * 3600, # 7 days
'item_list': 5 * 60, # 5 minutes
'item_detail': 10 * 60, # 10 minutes
'borrowing': 60, # 1 minute
'qrcode': 30 * 24 * 3600, # 30 days
'search': 2 * 60, # 2 minutes
'admin_agg': 10 * 60, # 10 minutes
'filters': 60 * 60, # 1 hour
}
def _get_cache_key(self, tenant_id, category, query_hash):
"""Generate cache key with tenant isolation."""
return f"cache:{tenant_id}:{category}:{query_hash}"
def _hash_query(self, query_dict):
"""Hash MongoDB query for cache key."""
query_str = json.dumps(query_dict, sort_keys=True, default=str)
return hashlib.md5(query_str.encode()).hexdigest()[:16]
def get(self, tenant_id, category, query_dict):
"""
Retrieve cached query result.
Returns None if not cached or expired.
"""
if not self.redis:
return None
try:
cache_key = self._get_cache_key(
tenant_id,
category,
self._hash_query(query_dict)
)
cached = self.redis.get(cache_key)
if cached:
logger.debug(f"Cache HIT: {category} for tenant {tenant_id}")
return json.loads(cached)
else:
logger.debug(f"Cache MISS: {category} for tenant {tenant_id}")
return None
except Exception as e:
logger.error(f"Cache retrieval failed: {e}")
return None
def set(self, tenant_id, category, query_dict, result, ttl=None):
"""
Cache query result with automatic expiration.
"""
if not self.redis:
return False
try:
cache_key = self._get_cache_key(
tenant_id,
category,
self._hash_query(query_dict)
)
ttl = ttl or self.ttls.get(category, 5 * 60)
self.redis.setex(
cache_key,
ttl,
json.dumps(result, default=str)
)
logger.debug(f"Cache SET: {category} for tenant {tenant_id} (TTL: {ttl}s)")
return True
except Exception as e:
logger.error(f"Cache write failed: {e}")
return False
def invalidate_category(self, tenant_id, category):
"""
Invalidate all cache entries in a category for a tenant.
Called after write operations (insert, update, delete).
"""
if not self.redis:
return False
try:
pattern = f"cache:{tenant_id}:{category}:*"
keys = self.redis.keys(pattern)
if keys:
deleted = self.redis.delete(*keys)
logger.info(f"Invalidated {deleted} cache entries: {category} for tenant {tenant_id}")
return deleted > 0
return False
except Exception as e:
logger.error(f"Cache invalidation failed: {e}")
return False
def invalidate_tenant(self, tenant_id):
"""
Completely clear all cache for a tenant.
Heavy operation - use sparingly.
"""
if not self.redis:
return False
try:
pattern = f"cache:{tenant_id}:*"
keys = self.redis.keys(pattern)
if keys:
deleted = self.redis.delete(*keys)
logger.warning(f"Cleared {deleted} cache entries for tenant {tenant_id}")
return deleted > 0
return False
except Exception as e:
logger.error(f"Tenant cache clear failed: {e}")
return False
def get_stats(self, tenant_id):
"""
Get cache statistics for tenant.
Useful for monitoring.
"""
if not self.redis:
return {}
try:
pattern = f"cache:{tenant_id}:*"
keys = self.redis.keys(pattern)
stats = {
'tenant_id': tenant_id,
'entries': len(keys),
'memory_bytes': sum(self.redis.memory_usage(k) or 0 for k in keys),
'categories': {}
}
# Count by category
for key in keys:
parts = key.split(':')
if len(parts) >= 3:
category = parts[2]
stats['categories'][category] = stats['categories'].get(category, 0) + 1
return stats
except Exception as e:
logger.error(f"Cache stats failed: {e}")
return {}
def get_cache_manager():
"""
Get or create cache manager for current request.
Safe to call outside request context.
"""
if not has_request_context():
return None
if 'cache_manager' not in g:
from session_manager import create_redis_session_interface
# Reuse Redis connection if available
interface = create_redis_session_interface(None)
if interface.redis:
# Use separate DB for cache (DB 1 instead of 0 for sessions)
g.cache_manager = CacheManager(
redis_client=interface.redis,
redis_db=1
)
else:
g.cache_manager = CacheManager()
return g.cache_manager
def cached_query(category='item_list', ttl=None):
"""
Decorator to cache MongoDB query results.
Usage:
@cached_query(category='item_list', ttl=300)
def get_items(db, filters):
return db['items'].find(filters).to_list(100)
"""
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
# Extract tenant from context
from tenant import get_tenant_context
ctx = get_tenant_context()
if not ctx or not ctx.tenant_id:
# No tenant context, execute without caching
return f(*args, **kwargs)
# Build query hash from args/kwargs
query_dict = {'args': str(args), 'kwargs': kwargs}
# Try cache
cache_mgr = get_cache_manager()
if cache_mgr:
cached_result = cache_mgr.get(ctx.tenant_id, category, query_dict)
if cached_result is not None:
return cached_result
# Execute function
result = f(*args, **kwargs)
# Cache result
if cache_mgr and result:
cache_mgr.set(ctx.tenant_id, category, query_dict, result, ttl)
return result
return decorated
return decorator
def invalidate_cache(tenant_id, category):
"""
Manually invalidate cache after write operations.
Usage in app.py:
# After deleting an item
invalidate_cache(tenant_id, 'item_list')
invalidate_cache(tenant_id, 'item_detail')
"""
cache_mgr = get_cache_manager()
if cache_mgr:
cache_mgr.invalidate_category(tenant_id, category)