Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
403 lines
13 KiB
Python
403 lines
13 KiB
Python
"""
|
|
Django integration for caching strategies.
|
|
Provides middleware, decorators, and template tags for easy caching.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from typing import Any, Dict, List, Optional, Union
|
|
from django.core.cache import cache
|
|
from django.http import HttpRequest, HttpResponse
|
|
from django.conf import settings
|
|
from django.contrib.auth import get_user_model
|
|
from django.contrib.auth.middleware import AuthenticationMiddleware
|
|
from django.utils.deprecation import MiddlewareMixin
|
|
from django.template import Library
|
|
from django.template.loader import render_to_string
|
|
from django.db import connection
|
|
from rest_framework.response import Response
|
|
from rest_framework.decorators import api_view
|
|
|
|
from .cache_manager import CacheManager, MalaysianDataCache, QueryCache
|
|
from .strategies import (
|
|
WriteThroughCache, WriteBehindCache, ReadThroughCache,
|
|
RefreshAheadCache, CacheAsidePattern, MultiLevelCache,
|
|
MalaysianCacheStrategies, cache_view_response, cache_query_results
|
|
)
|
|
from .config import CacheConfig
|
|
|
|
logger = logging.getLogger(__name__)
|
|
User = get_user_model()
|
|
register = Library()
|
|
|
|
|
|
class TenantCacheMiddleware(MiddlewareMixin):
|
|
"""Middleware for tenant-aware caching."""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.cache_manager = CacheManager()
|
|
self.malaysian_cache = MalaysianDataCache(self.cache_manager)
|
|
|
|
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
|
|
"""Process request with tenant-aware caching."""
|
|
# Add cache manager to request
|
|
request.cache_manager = self.cache_manager
|
|
request.malaysian_cache = self.malaysian_cache
|
|
|
|
# Cache tenant-specific data
|
|
if hasattr(request, 'tenant') and request.tenant:
|
|
tenant_key = f"tenant_data_{request.tenant.id}"
|
|
request.tenant_cache = self.cache_manager.get(tenant_key, {})
|
|
else:
|
|
request.tenant_cache = {}
|
|
|
|
return None
|
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
|
"""Process response with caching."""
|
|
# Add cache headers
|
|
response['X-Cache-Tenant'] = getattr(request, 'tenant', {}).get('schema_name', 'public')
|
|
response['X-Cache-Status'] = 'MISS' # Will be updated by cache middleware
|
|
|
|
return response
|
|
|
|
|
|
class CacheMiddleware(MiddlewareMixin):
|
|
"""Advanced caching middleware."""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.cache_manager = CacheManager()
|
|
self.cache_aside = CacheAsidePattern(self.cache_manager)
|
|
|
|
# Define cacheable paths and conditions
|
|
self.cacheable_paths = getattr(settings, 'CACHEABLE_PATHS', [
|
|
'/api/products/',
|
|
'/api/categories/',
|
|
'/api/static-data/',
|
|
])
|
|
|
|
self.non_cacheable_paths = getattr(settings, 'NON_CACHEABLE_PATHS', [
|
|
'/api/auth/',
|
|
'/api/admin/',
|
|
'/api/cart/',
|
|
'/api/orders/',
|
|
])
|
|
|
|
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
|
|
"""Process request with caching."""
|
|
if self._should_bypass_cache(request):
|
|
return None
|
|
|
|
cache_key = self._generate_cache_key(request)
|
|
cached_response = self.cache_manager.get(cache_key)
|
|
|
|
if cached_response:
|
|
response = HttpResponse(cached_response['content'])
|
|
response['X-Cache-Status'] = 'HIT'
|
|
response['Content-Type'] = cached_response.get('content_type', 'application/json')
|
|
return response
|
|
|
|
return None
|
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
|
"""Process response with caching."""
|
|
if self._should_bypass_cache(request) or getattr(response, '_cache_exempt', False):
|
|
response['X-Cache-Status'] = 'BYPASS'
|
|
return response
|
|
|
|
if self._should_cache_response(request, response):
|
|
cache_key = self._generate_cache_key(request)
|
|
cache_data = {
|
|
'content': response.content,
|
|
'content_type': response.get('Content-Type', 'application/json'),
|
|
'status_code': response.status_code,
|
|
}
|
|
|
|
timeout = self._get_cache_timeout(request)
|
|
self.cache_manager.set(cache_key, cache_data, timeout)
|
|
response['X-Cache-Status'] = 'MISS'
|
|
|
|
return response
|
|
|
|
def _should_bypass_cache(self, request: HttpRequest) -> bool:
|
|
"""Check if request should bypass cache."""
|
|
# Never cache authenticated user requests by default
|
|
if request.user.is_authenticated:
|
|
if getattr(settings, 'CACHE_AUTHENTICATED_REQUESTS', False):
|
|
return False
|
|
return True
|
|
|
|
# Check method
|
|
if request.method not in ['GET', 'HEAD']:
|
|
return True
|
|
|
|
# Check paths
|
|
for path in self.non_cacheable_paths:
|
|
if request.path.startswith(path):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _should_cache_response(self, request: HttpRequest, response: HttpResponse) -> bool:
|
|
"""Check if response should be cached."""
|
|
if response.status_code != 200:
|
|
return False
|
|
|
|
# Check cacheable paths
|
|
for path in self.cacheable_paths:
|
|
if request.path.startswith(path):
|
|
return True
|
|
|
|
return False
|
|
|
|
def _generate_cache_key(self, request: HttpRequest) -> str:
|
|
"""Generate cache key for request."""
|
|
key_parts = [
|
|
request.path,
|
|
request.method,
|
|
]
|
|
|
|
if request.GET:
|
|
key_parts.append(str(sorted(request.GET.items())))
|
|
|
|
# Add user info if authenticated
|
|
if request.user.is_authenticated:
|
|
key_parts.append(f"user_{request.user.id}")
|
|
|
|
# Add tenant info
|
|
if hasattr(request, 'tenant'):
|
|
key_parts.append(f"tenant_{request.tenant.id}")
|
|
|
|
key = "|".join(key_parts)
|
|
return f"view_cache_{hash(key)}"
|
|
|
|
def _get_cache_timeout(self, request: HttpRequest) -> int:
|
|
"""Get cache timeout for request."""
|
|
# Different timeouts for different paths
|
|
if request.path.startswith('/api/static-data/'):
|
|
return 3600 # 1 hour for static data
|
|
elif request.path.startswith('/api/products/'):
|
|
return 1800 # 30 minutes for products
|
|
else:
|
|
return 300 # 5 minutes default
|
|
|
|
|
|
class DatabaseCacheMiddleware(MiddlewareMixin):
|
|
"""Middleware for database query caching."""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.cache_manager = CacheManager()
|
|
self.query_cache = QueryCache(self.cache_manager)
|
|
self.queries_executed = []
|
|
self.cache_hits = 0
|
|
|
|
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
|
|
"""Initialize query tracking."""
|
|
self.queries_executed = []
|
|
self.cache_hits = 0
|
|
|
|
# Add query cache to request
|
|
request.query_cache = self.query_cache
|
|
|
|
return None
|
|
|
|
def process_response(self, request: HttpRequest, response: HttpResponse) -> HttpResponse:
|
|
"""Add query cache statistics to response."""
|
|
response['X-Cache-Queries'] = str(len(self.queries_executed))
|
|
response['X-Cache-Query-Hits'] = str(self.cache_hits)
|
|
|
|
return response
|
|
|
|
|
|
class MalaysianCacheMiddleware(MiddlewareMixin):
|
|
"""Middleware for Malaysian-specific caching."""
|
|
|
|
def __init__(self, get_response):
|
|
self.get_response = get_response
|
|
self.cache_manager = CacheManager()
|
|
self.malaysian_cache = MalaysianDataCache(self.cache_manager)
|
|
self.malaysian_strategies = MalaysianCacheStrategies(self.cache_manager)
|
|
|
|
def process_request(self, request: HttpRequest) -> Optional[HttpResponse]:
|
|
"""Add Malaysian cache to request."""
|
|
request.malaysian_cache = self.malaysian_cache
|
|
request.malaysian_strategies = self.malaysian_strategies
|
|
|
|
return None
|
|
|
|
|
|
# Template Tags
|
|
@register.simple_tag
|
|
def get_cached_postcode(postcode: str) -> str:
|
|
"""Get cached postcode data in template."""
|
|
cache_manager = CacheManager()
|
|
malaysian_cache = MalaysianDataCache(cache_manager)
|
|
|
|
data = malaysian_cache.get_cached_postcode_data(postcode)
|
|
if data:
|
|
return f"{data.get('city', 'Unknown')}, {data.get('state', 'Unknown')}"
|
|
return "Unknown location"
|
|
|
|
|
|
@register.simple_tag
|
|
def get_cached_sst_rate(state: str, category: str = "standard") -> str:
|
|
"""Get cached SST rate in template."""
|
|
cache_manager = CacheManager()
|
|
malaysian_cache = MalaysianDataCache(cache_manager)
|
|
|
|
rate = malaysian_cache.get_cached_sst_rate(state, category)
|
|
if rate is not None:
|
|
return f"{rate * 100:.0f}%"
|
|
return "Rate not available"
|
|
|
|
|
|
@register.simple_tag
|
|
def get_user_cache_info(user) -> str:
|
|
"""Get user cache information."""
|
|
if not user or not user.is_authenticated:
|
|
return "Anonymous"
|
|
|
|
cache_manager = CacheManager()
|
|
key = f"user_profile_{user.id}"
|
|
cached_data = cache_manager.get(key)
|
|
|
|
if cached_data:
|
|
return f"Cached user data available for {user.username}"
|
|
return f"No cached data for {user.username}"
|
|
|
|
|
|
# API Views
|
|
@api_view(['GET'])
|
|
def cache_stats(request):
|
|
"""Get cache statistics."""
|
|
if not request.user.is_staff:
|
|
return Response({"error": "Unauthorized"}, status=403)
|
|
|
|
cache_manager = CacheManager()
|
|
stats = cache_manager.get_cache_stats()
|
|
|
|
return Response(stats)
|
|
|
|
|
|
@api_view(['POST'])
|
|
def clear_cache(request):
|
|
"""Clear cache."""
|
|
if not request.user.is_staff:
|
|
return Response({"error": "Unauthorized"}, status=403)
|
|
|
|
cache_manager = CacheManager()
|
|
|
|
# Clear specific cache keys
|
|
cache_type = request.data.get('type', 'all')
|
|
|
|
if cache_type == 'tenant':
|
|
tenant_id = request.data.get('tenant_id')
|
|
success = cache_manager.clear_tenant_cache(tenant_id)
|
|
elif cache_type == 'malaysian':
|
|
# Clear Malaysian data cache
|
|
success = cache_manager.clear_tenant_cache()
|
|
else:
|
|
# Clear all cache
|
|
success = cache_manager.clear_tenant_cache()
|
|
|
|
if success:
|
|
return Response({"message": f"Cache cleared successfully"})
|
|
else:
|
|
return Response({"error": "Failed to clear cache"}, status=500)
|
|
|
|
|
|
@api_view(['GET'])
|
|
def warm_cache(request):
|
|
"""Warm cache with frequently accessed data."""
|
|
if not request.user.is_staff:
|
|
return Response({"error": "Unauthorized"}, status=403)
|
|
|
|
from .cache_manager import cache_warmer
|
|
|
|
# Warm Malaysian data
|
|
warmed = cache_warmer.warm_malaysian_data()
|
|
|
|
# Warm user data if specified
|
|
user_ids = request.GET.getlist('user_ids')
|
|
if user_ids:
|
|
user_ids = [int(uid) for uid in user_ids]
|
|
warmed_users = cache_warmer.warm_user_data(user_ids)
|
|
warmed['users'] = warmed_users
|
|
|
|
return Response({
|
|
"message": "Cache warming completed",
|
|
"warmed_items": warmed
|
|
})
|
|
|
|
|
|
# Django Settings Integration
|
|
def get_cache_config() -> Dict[str, Any]:
|
|
"""Get cache configuration for Django settings."""
|
|
return {
|
|
'CACHES': {
|
|
'default': {
|
|
'BACKEND': 'django_redis.cache.RedisCache',
|
|
'LOCATION': getattr(settings, 'REDIS_URL', 'redis://127.0.0.1:6379/1'),
|
|
'OPTIONS': {
|
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
|
},
|
|
'KEY_PREFIX': 'malaysian_sme_',
|
|
},
|
|
'locmem': {
|
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
'LOCATION': 'unique-snowflake',
|
|
},
|
|
},
|
|
'CACHE_MIDDLEWARE_ALIAS': 'default',
|
|
'CACHE_MIDDLEWARE_SECONDS': 300,
|
|
'CACHE_MIDDLEWARE_KEY_PREFIX': 'malaysian_sme_',
|
|
}
|
|
|
|
|
|
# Signal Handlers
|
|
def invalidate_user_cache(sender, instance, **kwargs):
|
|
"""Invalidate cache when user is updated."""
|
|
cache_manager = CacheManager()
|
|
cache_key = f"user_profile_{instance.id}"
|
|
cache_manager.delete(cache_key)
|
|
|
|
# Clear tenant cache if user is tenant owner
|
|
if hasattr(instance, 'owned_tenants'):
|
|
for tenant in instance.owned_tenants.all():
|
|
cache_manager.clear_tenant_cache(tenant.id)
|
|
|
|
|
|
def invalidate_model_cache(sender, instance, **kwargs):
|
|
"""Invalidate cache when model is updated."""
|
|
cache_manager = CacheManager()
|
|
query_cache = QueryCache(cache_manager)
|
|
|
|
model_name = instance.__class__.__name__.lower()
|
|
query_cache.invalidate_model_cache(model_name)
|
|
|
|
|
|
# Django Admin Integration
|
|
class CacheAdminMixin:
|
|
"""Mixin for Django admin cache management."""
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Override save to invalidate cache."""
|
|
super().save_model(request, obj, form, change)
|
|
|
|
# Invalidate cache
|
|
cache_manager = CacheManager()
|
|
model_name = obj.__class__.__name__.lower()
|
|
query_cache = QueryCache(cache_manager)
|
|
query_cache.invalidate_model_cache(model_name)
|
|
|
|
def delete_model(self, request, obj):
|
|
"""Override delete to invalidate cache."""
|
|
cache_manager = CacheManager()
|
|
model_name = obj.__class__.__name__.lower()
|
|
query_cache = QueryCache(cache_manager)
|
|
query_cache.invalidate_model_cache(model_name)
|
|
|
|
super().delete_model(request, obj) |