0c27d7ac86
- 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.
290 lines
8.8 KiB
Python
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)
|