project initialization
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

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,429 @@
"""
Multi-tenant caching strategies for Malaysian SME SaaS platform.
Provides advanced caching with Malaysian-specific optimizations.
"""
import json
import logging
import hashlib
import pickle
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Union, Tuple
from django.core.cache import cache
from django.conf import settings
from django.db import connection, models
from django_redis import get_redis_connection
from django.contrib.auth import get_user_model
from django.core.serializers.json import DjangoJSONEncoder
from django.utils import timezone
from django_tenants.utils import get_tenant_model, get_public_schema_name, get_tenant_schema_name
from rest_framework.response import Response
from .config import DatabaseConfig, CacheConfig
logger = logging.getLogger(__name__)
User = get_user_model()
TenantModel = get_tenant_model()
class CacheKeyGenerator:
"""Generates cache keys with multi-tenant support and Malaysian context."""
def __init__(self):
self.tenant_prefix = self._get_tenant_prefix()
self.malaysia_prefix = "my_sme"
def _get_tenant_prefix(self) -> str:
"""Get current tenant prefix for cache keys."""
try:
tenant = TenantModel.objects.get(schema_name=connection.schema_name)
return f"tenant_{tenant.id}"
except Exception:
return "public"
def generate_key(
self,
key_type: str,
identifier: str,
subkey: Optional[str] = None,
context: Optional[Dict[str, Any]] = None
) -> str:
"""Generate standardized cache key."""
components = [
self.malaysia_prefix,
self.tenant_prefix,
key_type,
identifier
]
if subkey:
components.append(subkey)
if context:
context_hash = hashlib.md5(
json.dumps(context, sort_keys=True).encode()
).hexdigest()[:8]
components.append(context_hash)
return ":".join(components)
def generate_malaysian_key(
self,
entity_type: str,
identifier: Union[str, int],
malaysian_context: Optional[Dict[str, Any]] = None
) -> str:
"""Generate Malaysian-specific cache key."""
return self.generate_key(
"my",
f"{entity_type}_{identifier}",
context=malaysian_context
)
class CacheManager:
"""Advanced cache management with multi-tenant support."""
def __init__(self, config: Optional[CacheConfig] = None):
self.config = config or CacheConfig()
self.key_generator = CacheKeyGenerator()
self.redis_client = None
if self.config.use_redis:
try:
self.redis_client = get_redis_connection("default")
except Exception as e:
logger.warning(f"Redis connection failed: {e}")
def get(
self,
key: str,
default: Any = None,
version: Optional[int] = None
) -> Any:
"""Get value from cache with error handling."""
try:
return cache.get(key, default=default, version=version)
except Exception as e:
logger.error(f"Cache get error for key {key}: {e}")
return default
def set(
self,
key: str,
value: Any,
timeout: Optional[int] = None,
version: Optional[int] = None
) -> bool:
"""Set value in cache with error handling."""
try:
timeout = timeout or self.config.default_timeout
return cache.set(key, value, timeout=timeout, version=version)
except Exception as e:
logger.error(f"Cache set error for key {key}: {e}")
return False
def delete(self, key: str, version: Optional[int] = None) -> bool:
"""Delete key from cache."""
try:
return cache.delete(key, version=version)
except Exception as e:
logger.error(f"Cache delete error for key {key}: {e}")
return False
def clear_tenant_cache(self, tenant_id: Optional[int] = None) -> bool:
"""Clear all cache for a specific tenant."""
try:
if tenant_id:
pattern = f"*:tenant_{tenant_id}:*"
else:
pattern = f"*:{self.key_generator.tenant_prefix}:*"
if self.redis_client:
keys = self.redis_client.keys(pattern)
if keys:
self.redis_client.delete(*keys)
return True
except Exception as e:
logger.error(f"Error clearing tenant cache: {e}")
return False
def get_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics."""
stats = {
"tenant": self.key_generator.tenant_prefix,
"redis_available": self.redis_client is not None,
"default_timeout": self.config.default_timeout,
}
if self.redis_client:
try:
info = self.redis_client.info()
stats.update({
"used_memory": info.get("used_memory_human", "N/A"),
"connected_clients": info.get("connected_clients", 0),
"total_commands_processed": info.get("total_commands_processed", 0),
})
except Exception as e:
logger.error(f"Error getting Redis stats: {e}")
return stats
class MalaysianDataCache:
"""Specialized caching for Malaysian data and validations."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
def get_cached_ic_validation(self, ic_number: str) -> Optional[Dict[str, Any]]:
"""Get cached IC validation result."""
key = self.cache.key_generator.generate_malaysian_key(
"ic_validation",
ic_number
)
return self.cache.get(key)
def set_cached_ic_validation(
self,
ic_number: str,
validation_result: Dict[str, Any]
) -> bool:
"""Cache IC validation result."""
key = self.cache.key_generator.generate_malaysian_key(
"ic_validation",
ic_number
)
return self.cache.set(key, validation_result, timeout=86400) # 24 hours
def get_cached_sst_rate(self, state: str, category: str) -> Optional[float]:
"""Get cached SST rate."""
key = self.cache.key_generator.generate_malaysian_key(
"sst_rate",
f"{state}_{category}"
)
return self.cache.get(key)
def set_cached_sst_rate(
self,
state: str,
category: str,
rate: float
) -> bool:
"""Cache SST rate."""
key = self.cache.key_generator.generate_malaysian_key(
"sst_rate",
f"{state}_{category}"
)
return self.cache.set(key, rate, timeout=604800) # 7 days
def get_cached_postcode_data(self, postcode: str) -> Optional[Dict[str, Any]]:
"""Get cached postcode data."""
key = self.cache.key_generator.generate_malaysian_key(
"postcode",
postcode
)
return self.cache.get(key)
def set_cached_postcode_data(
self,
postcode: str,
postcode_data: Dict[str, Any]
) -> bool:
"""Cache postcode data."""
key = self.cache.key_generator.generate_malaysian_key(
"postcode",
postcode
)
return self.cache.set(key, postcode_data, timeout=2592000) # 30 days
class QueryCache:
"""Intelligent query caching with automatic invalidation."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
self.query_hashes = set()
def generate_query_hash(self, query: str, params: Optional[tuple] = None) -> str:
"""Generate hash for query identification."""
query_string = query.strip().lower()
if params:
query_string += str(params)
return hashlib.md5(query_string.encode()).hexdigest()
def cache_query_result(
self,
query: str,
result: Any,
params: Optional[tuple] = None,
timeout: Optional[int] = None
) -> bool:
"""Cache query result."""
query_hash = self.generate_query_hash(query, params)
key = self.cache.key_generator.generate_key("query", query_hash)
success = self.cache.set(key, result, timeout=timeout)
if success:
self.query_hashes.add(query_hash)
return success
def get_cached_query_result(
self,
query: str,
params: Optional[tuple] = None
) -> Optional[Any]:
"""Get cached query result."""
query_hash = self.generate_query_hash(query, params)
key = self.cache.key_generator.generate_key("query", query_hash)
return self.cache.get(key)
def invalidate_model_cache(self, model_name: str) -> int:
"""Invalidate cache for a specific model."""
invalidated = 0
for query_hash in list(self.query_hashes):
if model_name.lower() in query_hash:
key = self.cache.key_generator.generate_key("query", query_hash)
if self.cache.delete(key):
invalidated += 1
self.query_hashes.discard(query_hash)
return invalidated
class TenantCacheManager:
"""Multi-tenant cache management with isolation."""
def __init__(self):
self.cache_managers = {}
def get_cache_manager(self, tenant_id: Optional[int] = None) -> CacheManager:
"""Get cache manager for specific tenant."""
if not tenant_id:
tenant_id = self._get_current_tenant_id()
if tenant_id not in self.cache_managers:
config = CacheConfig()
config.tenant_isolation = True
config.tenant_prefix = f"tenant_{tenant_id}"
self.cache_managers[tenant_id] = CacheManager(config)
return self.cache_managers[tenant_id]
def _get_current_tenant_id(self) -> int:
"""Get current tenant ID."""
try:
tenant = TenantModel.objects.get(schema_name=connection.schema_name)
return tenant.id
except Exception:
return 0 # Public schema
def clear_all_tenant_cache(self) -> Dict[str, Any]:
"""Clear cache for all tenants."""
results = {"cleared_tenants": 0, "errors": []}
for tenant_id, cache_manager in self.cache_managers.items():
try:
if cache_manager.clear_tenant_cache(tenant_id):
results["cleared_tenants"] += 1
except Exception as e:
results["errors"].append(f"Tenant {tenant_id}: {e}")
return results
def get_tenant_cache_stats(self) -> Dict[str, Any]:
"""Get cache statistics for all tenants."""
stats = {"tenants": {}, "total_tenants": len(self.cache_managers)}
for tenant_id, cache_manager in self.cache_managers.items():
stats["tenants"][str(tenant_id)] = cache_manager.get_cache_stats()
return stats
class CacheWarmer:
"""Proactive cache warming for critical data."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
self.malaysian_cache = MalaysianDataCache(cache_manager)
def warm_malaysian_data(self) -> Dict[str, int]:
"""Warm cache with Malaysian reference data."""
warmed = {"ic_validations": 0, "sst_rates": 0, "postcodes": 0}
# Warm SST rates
sst_rates = self._get_sst_rates_to_warm()
for state, category, rate in sst_rates:
if self.malaysian_cache.set_cached_sst_rate(state, category, rate):
warmed["sst_rates"] += 1
# Warm postcode data
postcodes = self._get_postcodes_to_warm()
for postcode, data in postcodes:
if self.malaysian_cache.set_cached_postcode_data(postcode, data):
warmed["postcodes"] += 1
return warmed
def warm_user_data(self, user_ids: List[int]) -> int:
"""Warm cache with user data."""
warmed = 0
for user_id in user_ids:
try:
user = User.objects.get(id=user_id)
key = self.cache.key_generator.generate_key("user", str(user_id))
user_data = {
"id": user.id,
"username": user.username,
"email": user.email,
"is_active": user.is_active,
"last_login": user.last_login,
}
if self.cache.set(key, user_data):
warmed += 1
except User.DoesNotExist:
continue
return warmed
def _get_sst_rates_to_warm(self) -> List[Tuple[str, str, float]]:
"""Get SST rates to warm in cache."""
# Common Malaysian states and categories
states = ["Johor", "Kedah", "Kelantan", "Melaka", "Negeri Sembilan",
"Pahang", "Perak", "Perlis", "Pulau Pinang", "Sabah",
"Sarawak", "Selangor", "Terengganu", "WP Kuala Lumpur",
"WP Labuan", "WP Putrajaya"]
categories = ["standard", "food", "medical", "education"]
rates = []
for state in states:
for category in categories:
rate = 0.06 if category == "standard" else 0.0
rates.append((state, category, rate))
return rates
def _get_postcodes_to_warm(self) -> List[Tuple[str, Dict[str, Any]]]:
"""Get postcode data to warm in cache."""
# Common Malaysian postcodes
postcodes = [
("50000", {"city": "Kuala Lumpur", "state": "WP Kuala Lumpur"}),
("50480", {"city": "Kuala Lumpur", "state": "WP Kuala Lumpur"}),
("80000", {"city": "Johor Bahru", "state": "Johor"}),
("93000", {"city": "Kuching", "state": "Sarawak"}),
("88300", {"city": "Kota Kinabalu", "state": "Sabah"}),
]
return postcodes
# Global instances
tenant_cache_manager = TenantCacheManager()
cache_manager = CacheManager()
malaysian_cache = MalaysianDataCache(cache_manager)
query_cache = QueryCache(cache_manager)
cache_warmer = CacheWarmer(cache_manager)

View File

@@ -0,0 +1,403 @@
"""
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)

View File

@@ -0,0 +1,399 @@
"""
Advanced caching strategies for Malaysian SME SaaS platform.
Implements various caching patterns and optimizations.
"""
import json
import logging
import threading
import time
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Union, Callable, Tuple
from functools import wraps
from django.core.cache import cache
from django.db import connection, transaction
from django.conf import settings
from django.utils import timezone
from django.http import HttpRequest, HttpResponse
from django.contrib.auth import get_user_model
from rest_framework.response import Response
from rest_framework.decorators import api_view
from django.views.decorators.cache import cache_page
from django.views.decorators.vary import vary_on_headers, vary_on_cookie
from .cache_manager import CacheManager, MalaysianDataCache, QueryCache
from .config import CacheConfig
logger = logging.getLogger(__name__)
User = get_user_model()
class CacheStrategy:
"""Base class for caching strategies."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
self.hits = 0
self.misses = 0
self.evictions = 0
def get(self, key: str, default: Any = None) -> Any:
"""Get value from cache."""
result = self.cache.get(key, default)
if result == default:
self.misses += 1
else:
self.hits += 1
return result
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
"""Set value in cache."""
return self.cache.set(key, value, timeout)
def get_stats(self) -> Dict[str, Any]:
"""Get strategy statistics."""
return {
"hits": self.hits,
"misses": self.misses,
"hit_rate": self.hits / (self.hits + self.misses) if (self.hits + self.misses) > 0 else 0,
"evictions": self.evictions
}
class WriteThroughCache(CacheStrategy):
"""Write-through caching pattern."""
def write_through(self, key: str, value: Any, db_operation: Callable, timeout: Optional[int] = None) -> Any:
"""Write through cache and database."""
try:
# Write to database
result = db_operation()
# Write to cache
self.set(key, result, timeout)
return result
except Exception as e:
logger.error(f"Write-through cache error: {e}")
raise
class WriteBehindCache(CacheStrategy):
"""Write-behind caching pattern with async writing."""
def __init__(self, cache_manager: CacheManager, batch_size: int = 10):
super().__init__(cache_manager)
self.batch_size = batch_size
self.write_queue = []
self.write_lock = threading.Lock()
self.writer_thread = threading.Thread(target=self._batch_writer, daemon=True)
self.writer_thread.start()
def write_behind(self, key: str, value: Any, db_operation: Callable) -> bool:
"""Write to cache and queue for database."""
try:
# Write to cache immediately
self.set(key, value)
# Queue for database write
with self.write_lock:
self.write_queue.append((key, value, db_operation))
return True
except Exception as e:
logger.error(f"Write-behind cache error: {e}")
return False
def _batch_writer(self):
"""Background thread for batch database writes."""
while True:
time.sleep(5) # Write every 5 seconds
if not self.write_queue:
continue
batch = []
with self.write_lock:
batch = self.write_queue[:self.batch_size]
self.write_queue = self.write_queue[self.batch_size:]
for key, value, db_operation in batch:
try:
db_operation(value)
except Exception as e:
logger.error(f"Batch write error for key {key}: {e}")
class ReadThroughCache(CacheStrategy):
"""Read-through caching pattern."""
def read_through(self, key: str, db_operation: Callable, timeout: Optional[int] = None) -> Any:
"""Read through cache with fallback to database."""
result = self.get(key)
if result is not None:
return result
try:
# Read from database
result = db_operation()
# Cache the result
if result is not None:
self.set(key, result, timeout)
return result
except Exception as e:
logger.error(f"Read-through cache error: {e}")
raise
class RefreshAheadCache(CacheStrategy):
"""Refresh-ahead caching pattern."""
def __init__(self, cache_manager: CacheManager, refresh_interval: int = 300):
super().__init__(cache_manager)
self.refresh_interval = refresh_interval
self.refresh_queue = set()
self.refresh_lock = threading.Lock()
self.refresh_thread = threading.Thread(target=self._refresh_worker, daemon=True)
self.refresh_thread.start()
def get_or_refresh(self, key: str, db_operation: Callable, timeout: Optional[int] = None) -> Any:
"""Get from cache and queue for refresh if needed."""
result = self.get(key)
if result is not None:
# Queue for refresh
with self.refresh_lock:
self.refresh_queue.add((key, db_operation, timeout))
return result
# Cache miss - get from database
try:
result = db_operation()
if result is not None:
self.set(key, result, timeout)
return result
except Exception as e:
logger.error(f"Refresh-ahead cache error: {e}")
raise
def _refresh_worker(self):
"""Background thread for cache refresh."""
while True:
time.sleep(self.refresh_interval)
if not self.refresh_queue:
continue
items_to_refresh = []
with self.refresh_lock:
items_to_refresh = list(self.refresh_queue)
self.refresh_queue.clear()
for key, db_operation, timeout in items_to_refresh:
try:
result = db_operation()
if result is not None:
self.set(key, result, timeout)
except Exception as e:
logger.error(f"Refresh error for key {key}: {e}")
class CacheAsidePattern:
"""Cache-aside pattern implementation."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
def get_or_set(self, key: str, db_operation: Callable, timeout: Optional[int] = None) -> Any:
"""Get from cache or set if not exists."""
result = self.cache.get(key)
if result is not None:
return result
try:
result = db_operation()
if result is not None:
self.cache.set(key, result, timeout)
return result
except Exception as e:
logger.error(f"Cache-aside pattern error: {e}")
raise
def invalidate(self, key: str) -> bool:
"""Invalidate cache key."""
return self.cache.delete(key)
class MultiLevelCache:
"""Multi-level caching with L1 and L2 caches."""
def __init__(self, l1_cache: CacheManager, l2_cache: CacheManager):
self.l1_cache = l1_cache
self.l2_cache = l2_cache
self.l1_hits = 0
self.l2_hits = 0
self.misses = 0
def get(self, key: str) -> Optional[Any]:
"""Get from multi-level cache."""
# Try L1 cache first
result = self.l1_cache.get(key)
if result is not None:
self.l1_hits += 1
return result
# Try L2 cache
result = self.l2_cache.get(key)
if result is not None:
self.l2_hits += 1
# Promote to L1 cache
self.l1_cache.set(key, result)
return result
self.misses += 1
return None
def set(self, key: str, value: Any, timeout: Optional[int] = None) -> bool:
"""Set in both cache levels."""
l1_success = self.l1_cache.set(key, value, timeout)
l2_success = self.l2_cache.set(key, value, timeout)
return l1_success and l2_success
def get_stats(self) -> Dict[str, Any]:
"""Get multi-level cache statistics."""
return {
"l1_hits": self.l1_hits,
"l2_hits": self.l2_hits,
"misses": self.misses,
"total_requests": self.l1_hits + self.l2_hits + self.misses,
"l1_hit_rate": self.l1_hits / (self.l1_hits + self.l2_hits + self.misses) if (self.l1_hits + self.l2_hits + self.misses) > 0 else 0,
"overall_hit_rate": (self.l1_hits + self.l2_hits) / (self.l1_hits + self.l2_hits + self.misses) if (self.l1_hits + self.l2_hits + self.misses) > 0 else 0
}
class MalaysianCacheStrategies:
"""Malaysian-specific caching strategies."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
self.malaysian_cache = MalaysianDataCache(cache_manager)
self.query_cache = QueryCache(cache_manager)
def cache_ic_validation(self, ic_number: str, validation_func: Callable) -> Dict[str, Any]:
"""Cache IC validation results with TTL."""
cached_result = self.malaysian_cache.get_cached_ic_validation(ic_number)
if cached_result:
return cached_result
result = validation_func(ic_number)
self.malaysian_cache.set_cached_ic_validation(ic_number, result)
return result
def cache_sst_calculation(self, calculation_key: str, calculation_func: Callable) -> float:
"""Cache SST calculations."""
key = f"sst_calc_{calculation_key}"
cached_result = self.cache.get(key)
if cached_result:
return cached_result
result = calculation_func()
self.cache.set(key, result, timeout=3600) # 1 hour
return result
def cache_postcode_lookup(self, postcode: str, lookup_func: Callable) -> Dict[str, Any]:
"""Cache postcode lookups with extended TTL."""
cached_result = self.malaysian_cache.get_cached_postcode_data(postcode)
if cached_result:
return cached_result
result = lookup_func(postcode)
self.malaysian_cache.set_cached_postcode_data(postcode, result)
return result
# Decorators for easy caching
def cache_view_response(timeout: int = 300, key_prefix: str = ""):
"""Decorator to cache view responses."""
def decorator(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
cache_key = f"{key_prefix}_{request.path}_{request.user.id if request.user.is_authenticated else 'anonymous'}"
response = cache.get(cache_key)
if response:
return response
response = view_func(request, *args, **kwargs)
if isinstance(response, HttpResponse):
cache.set(cache_key, response, timeout)
return response
return _wrapped_view
return decorator
def cache_query_results(timeout: int = 300, key_func: Optional[Callable] = None):
"""Decorator to cache query results."""
def decorator(query_func):
@wraps(query_func)
def _wrapped_query(*args, **kwargs):
cache_key = key_func(*args, **kwargs) if key_func else f"query_{query_func.__name__}_{hash(str(args) + str(kwargs))}"
result = cache.get(cache_key)
if result:
return result
result = query_func(*args, **kwargs)
cache.set(cache_key, result, timeout)
return result
return _wrapped_query
return decorator
def invalidate_cache_on_save(model):
"""Decorator to invalidate cache when model is saved."""
def decorator(save_method):
@wraps(save_method)
def _wrapped_save(self, *args, **kwargs):
result = save_method(self, *args, **kwargs)
# Invalidate cache for this model
cache_key = f"{model.__name__}_{self.id}"
cache.delete(cache_key)
return result
return _wrapped_save
return decorator
class CacheEvictionPolicy:
"""Advanced cache eviction policies."""
def __init__(self, cache_manager: CacheManager):
self.cache = cache_manager
self.access_times = {}
self.access_counts = {}
def record_access(self, key: str):
"""Record key access for eviction policies."""
now = time.time()
self.access_times[key] = now
self.access_counts[key] = self.access_counts.get(key, 0) + 1
def lru_eviction(self, keys: List[str], count: int = 1) -> List[str]:
"""Least Recently Used eviction."""
sorted_keys = sorted(keys, key=lambda k: self.access_times.get(k, 0))
return sorted_keys[:count]
def lfu_eviction(self, keys: List[str], count: int = 1) -> List[str]:
"""Least Frequently Used eviction."""
sorted_keys = sorted(keys, key=lambda k: self.access_counts.get(k, 0))
return sorted_keys[:count]
def fifo_eviction(self, keys: List[str], count: int = 1) -> List[str]:
"""First In First Out eviction."""
return keys[:count]