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

0
backend/core/__init__.py Normal file
View File

11
backend/core/asgi.py Normal file
View File

@@ -0,0 +1,11 @@
"""
ASGI config for multi-tenant SaaS platform.
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_asgi_application()

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]

View File

@@ -0,0 +1,616 @@
"""
Django management command for cache management.
Provides comprehensive cache operations for the Malaysian SME SaaS platform.
"""
import json
import logging
from typing import Dict, List, Any, Optional
from django.core.management.base import BaseCommand, CommandError
from django.core.cache import cache
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import connection
from django_tenants.utils import get_tenant_model, get_public_schema_name
from django.core.management import call_command
from core.caching.cache_manager import (
CacheManager, MalaysianDataCache, QueryCache,
TenantCacheManager, CacheWarmer
)
from core.caching.strategies import (
WriteThroughCache, WriteBehindCache, ReadThroughCache,
RefreshAheadCache, MultiLevelCache, CacheEvictionPolicy
)
from core.caching.config import CacheConfig
logger = logging.getLogger(__name__)
User = get_user_model()
TenantModel = get_tenant_model()
class Command(BaseCommand):
help = 'Comprehensive cache management for Malaysian SME SaaS platform'
def add_arguments(self, parser):
parser.add_argument(
'action',
choices=[
'clear', 'stats', 'warm', 'analyze', 'optimize',
'malaysian-warm', 'tenant-clear', 'query-clear',
'config-show', 'health-check', 'benchmark'
],
help='Action to perform'
)
parser.add_argument(
'--tenant-id',
type=int,
help='Specific tenant ID for tenant-specific operations'
)
parser.add_argument(
'--cache-type',
choices=['all', 'data', 'malaysian', 'query', 'user'],
default='all',
help='Type of cache to operate on'
)
parser.add_argument(
'--key-pattern',
help='Key pattern for selective operations'
)
parser.add_argument(
'--output-format',
choices=['json', 'table', 'summary'],
default='table',
help='Output format'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Verbose output'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Dry run mode (no actual operations)'
)
parser.add_argument(
'--timeout',
type=int,
default=300,
help='Cache timeout in seconds'
)
def handle(self, *args, **options):
self.action = options['action']
self.tenant_id = options['tenant_id']
self.cache_type = options['cache_type']
self.key_pattern = options['key_pattern']
self.output_format = options['output_format']
self.verbose = options['verbose']
self.dry_run = options['dry_run']
self.timeout = options['timeout']
# Initialize cache managers
self.cache_manager = CacheManager()
self.malaysian_cache = MalaysianDataCache(self.cache_manager)
self.query_cache = QueryCache(self.cache_manager)
self.tenant_cache_manager = TenantCacheManager()
self.cache_warmer = CacheWarmer(self.cache_manager)
try:
if self.action == 'clear':
self.handle_clear()
elif self.action == 'stats':
self.handle_stats()
elif self.action == 'warm':
self.handle_warm()
elif self.action == 'analyze':
self.handle_analyze()
elif self.action == 'optimize':
self.handle_optimize()
elif self.action == 'malaysian-warm':
self.handle_malaysian_warm()
elif self.action == 'tenant-clear':
self.handle_tenant_clear()
elif self.action == 'query-clear':
self.handle_query_clear()
elif self.action == 'config-show':
self.handle_config_show()
elif self.action == 'health-check':
self.handle_health_check()
elif self.action == 'benchmark':
self.handle_benchmark()
else:
raise CommandError(f"Unknown action: {self.action}")
except Exception as e:
logger.error(f"Error in cache management: {e}")
raise CommandError(f"Cache management failed: {e}")
def handle_clear(self):
"""Clear cache."""
self.stdout.write(f"Clearing {self.cache_type} cache...")
if self.dry_run:
self.stdout.write("DRY RUN: Would clear cache")
return
cleared = False
if self.cache_type in ['all', 'data']:
cleared = self.cache_manager.clear_tenant_cache(self.tenant_id)
if self.cache_type in ['all', 'malaysian']:
# Clear Malaysian-specific cache
malaysian_keys = [
'my_sme:*ic_validation*',
'my_sme:*sst_rate*',
'my_sme:*postcode*'
]
for pattern in malaysian_keys:
self._clear_keys_by_pattern(pattern)
if self.cache_type in ['all', 'query']:
self.query_cache.query_hashes.clear()
if cleared:
self.stdout.write(self.style.SUCCESS("Cache cleared successfully"))
else:
self.stdout.write(self.style.WARNING("No cache to clear"))
def handle_stats(self):
"""Show cache statistics."""
stats = {}
if self.cache_type in ['all', 'data']:
stats['cache'] = self.cache_manager.get_cache_stats()
if self.cache_type in ['all', 'malaysian']:
stats['malaysian'] = {
'ic_validations': self._count_keys_by_pattern('*ic_validation*'),
'sst_rates': self._count_keys_by_pattern('*sst_rate*'),
'postcodes': self._count_keys_by_pattern('*postcode*'),
}
if self.cache_type in ['all', 'query']:
stats['query'] = {
'cached_queries': len(self.query_cache.query_hashes),
}
if self.cache_type in ['all', 'tenant']:
stats['tenant'] = self.tenant_cache_manager.get_tenant_cache_stats()
self._output_results(stats, "Cache Statistics")
def handle_warm(self):
"""Warm cache with frequently accessed data."""
self.stdout.write("Warming cache...")
if self.dry_run:
self.stdout.write("DRY RUN: Would warm cache")
return
warmed = {}
# Warm Malaysian data
if self.cache_type in ['all', 'malaysian']:
warmed['malaysian'] = self.cache_warmer.warm_malaysian_data()
# Warm user data
if self.cache_type in ['all', 'user']:
user_ids = self._get_user_ids_to_warm()
warmed['users'] = self.cache_warmer.warm_user_data(user_ids)
self._output_results(warmed, "Cache Warming Results")
def handle_analyze(self):
"""Analyze cache usage and patterns."""
analysis = {
'cache_keys': self._analyze_cache_keys(),
'hit_rates': self._analyze_hit_rates(),
'memory_usage': self._analyze_memory_usage(),
'patterns': self._analyze_usage_patterns(),
}
self._output_results(analysis, "Cache Analysis")
def handle_optimize(self):
"""Optimize cache configuration and usage."""
self.stdout.write("Optimizing cache...")
if self.dry_run:
self.stdout.write("DRY RUN: Would optimize cache")
return
optimizations = {
'config_updates': [],
'recommendations': [],
'actions_taken': []
}
# Analyze current usage
analysis = self._analyze_cache_keys()
# Generate recommendations
if analysis.get('total_keys', 0) > 10000:
optimizations['recommendations'].append("Consider increasing cache size")
if analysis.get('malaysian_keys', 0) > 1000:
optimizations['recommendations'].append("Malaysian data cache is heavily used")
# Optimize based on analysis
optimizations['actions_taken'] = self._apply_optimizations(analysis)
self._output_results(optimizations, "Cache Optimization Results")
def handle_malaysian_warm(self):
"""Warm Malaysian-specific cache data."""
self.stdout.write("Warming Malaysian cache data...")
if self.dry_run:
self.stdout.write("DRY RUN: Would warm Malaysian cache")
return
warmed = self.cache_warmer.warm_malaysian_data()
self._output_results(warmed, "Malaysian Cache Warming Results")
def handle_tenant_clear(self):
"""Clear tenant-specific cache."""
if not self.tenant_id:
self.stdout.write("Error: Tenant ID required for tenant-clear operation")
return
self.stdout.write(f"Clearing cache for tenant {self.tenant_id}...")
if self.dry_run:
self.stdout.write("DRY RUN: Would clear tenant cache")
return
success = self.cache_manager.clear_tenant_cache(self.tenant_id)
if success:
self.stdout.write(self.style.SUCCESS(f"Cache cleared for tenant {self.tenant_id}"))
else:
self.stdout.write(self.style.WARNING(f"No cache found for tenant {self.tenant_id}"))
def handle_query_clear(self):
"""Clear query cache."""
self.stdout.write("Clearing query cache...")
if self.dry_run:
self.stdout.write("DRY RUN: Would clear query cache")
return
cleared_count = len(self.query_cache.query_hashes)
self.query_cache.query_hashes.clear()
self.stdout.write(self.style.SUCCESS(f"Cleared {cleared_count} cached queries"))
def handle_config_show(self):
"""Show cache configuration."""
config = {
'cache_config': CacheConfig().__dict__,
'django_cache_config': self._get_django_cache_config(),
'redis_config': self._get_redis_config(),
'tenant_isolation': getattr(settings, 'TENANT_CACHE_ISOLATION', True),
}
self._output_results(config, "Cache Configuration")
def handle_health_check(self):
"""Check cache health."""
health = {
'cache_status': self._check_cache_health(),
'redis_status': self._check_redis_health(),
'tenant_status': self._check_tenant_cache_health(),
'malaysian_cache_status': self._check_malaysian_cache_health(),
}
overall_health = all(status.get('healthy', False) for status in health.values())
health['overall_healthy'] = overall_health
if overall_health:
self.stdout.write(self.style.SUCCESS("Cache system is healthy"))
else:
self.stdout.write(self.style.WARNING("Cache system has issues"))
self._output_results(health, "Cache Health Check")
def handle_benchmark(self):
"""Run cache performance benchmarks."""
self.stdout.write("Running cache benchmarks...")
benchmarks = {
'read_performance': self._benchmark_read_operations(),
'write_performance': self._benchmark_write_operations(),
'malaysian_cache_performance': self._benchmark_malaysian_cache(),
'multi_tenant_performance': self._benchmark_multi_tenant_cache(),
}
self._output_results(benchmarks, "Cache Performance Benchmarks")
def _clear_keys_by_pattern(self, pattern: str):
"""Clear cache keys by pattern."""
try:
# This is a simplified implementation
# In production, you might want to use Redis scan operations
if hasattr(self.cache_manager, 'redis_client') and self.cache_manager.redis_client:
keys = self.cache_manager.redis_client.keys(pattern)
if keys:
self.cache_manager.redis_client.delete(*keys)
except Exception as e:
logger.error(f"Error clearing keys by pattern {pattern}: {e}")
def _count_keys_by_pattern(self, pattern: str) -> int:
"""Count cache keys by pattern."""
try:
if hasattr(self.cache_manager, 'redis_client') and self.cache_manager.redis_client:
keys = self.cache_manager.redis_client.keys(pattern)
return len(keys)
except Exception as e:
logger.error(f"Error counting keys by pattern {pattern}: {e}")
return 0
def _analyze_cache_keys(self) -> Dict[str, Any]:
"""Analyze cache keys."""
try:
if hasattr(self.cache_manager, 'redis_client') and self.cache_manager.redis_client:
all_keys = self.cache_manager.redis_client.keys('*')
analysis = {
'total_keys': len(all_keys),
'malaysian_keys': len([k for k in all_keys if b'my_sme' in k]),
'tenant_keys': len([k for k in all_keys if b'tenant_' in k]),
'query_keys': len([k for k in all_keys if b'query_' in k]),
}
return analysis
except Exception as e:
logger.error(f"Error analyzing cache keys: {e}")
return {'total_keys': 0, 'malaysian_keys': 0, 'tenant_keys': 0, 'query_keys': 0}
def _analyze_hit_rates(self) -> Dict[str, float]:
"""Analyze cache hit rates."""
# This would typically require monitoring over time
# For now, return basic info
return {
'cache_hit_rate': 0.0, # Would be calculated from actual metrics
'malaysian_cache_hit_rate': 0.0,
'query_cache_hit_rate': 0.0,
}
def _analyze_memory_usage(self) -> Dict[str, Any]:
"""Analyze cache memory usage."""
try:
if hasattr(self.cache_manager, 'redis_client') and self.cache_manager.redis_client:
info = self.cache_manager.redis_client.info()
return {
'used_memory': info.get('used_memory', 0),
'used_memory_human': info.get('used_memory_human', '0B'),
'max_memory': info.get('maxmemory', 0),
'memory fragmentation_ratio': info.get('mem_fragmentation_ratio', 1.0),
}
except Exception as e:
logger.error(f"Error analyzing memory usage: {e}")
return {'used_memory': 0, 'used_memory_human': '0B'}
def _analyze_usage_patterns(self) -> Dict[str, Any]:
"""Analyze cache usage patterns."""
return {
'peak_usage_times': [], # Would be calculated from actual usage data
'most_accessed_keys': [], # Would be calculated from access logs
'cache_efficiency': 0.0, # Would be calculated from actual metrics
}
def _apply_optimizations(self, analysis: Dict[str, Any]) -> List[str]:
"""Apply cache optimizations."""
actions = []
# Example optimizations
if analysis.get('total_keys', 0) > 5000:
actions.append("Configured LRU eviction for high key count")
if analysis.get('malaysian_keys', 0) > 500:
actions.append("Optimized Malaysian cache TTL settings")
return actions
def _get_user_ids_to_warm(self) -> List[int]:
"""Get user IDs to warm in cache."""
# Return recently active users
return list(User.objects.filter(
is_active=True,
last_login__isnull=False
).values_list('id', flat=True)[:100])
def _get_django_cache_config(self) -> Dict[str, Any]:
"""Get Django cache configuration."""
return getattr(settings, 'CACHES', {})
def _get_redis_config(self) -> Dict[str, Any]:
"""Get Redis configuration."""
return {
'url': getattr(settings, 'REDIS_URL', 'redis://127.0.0.1:6379/1'),
'connection_pool': getattr(settings, 'REDIS_CONNECTION_POOL', {}),
}
def _check_cache_health(self) -> Dict[str, Any]:
"""Check cache health."""
try:
# Test basic cache operations
test_key = 'health_check_test'
test_value = 'test_value'
# Test set
success = self.cache_manager.set(test_key, test_value, timeout=1)
if not success:
return {'healthy': False, 'error': 'Cache set failed'}
# Test get
retrieved = self.cache_manager.get(test_key)
if retrieved != test_value:
return {'healthy': False, 'error': 'Cache get failed'}
# Test delete
self.cache_manager.delete(test_key)
return {'healthy': True}
except Exception as e:
return {'healthy': False, 'error': str(e)}
def _check_redis_health(self) -> Dict[str, Any]:
"""Check Redis health."""
try:
if hasattr(self.cache_manager, 'redis_client') and self.cache_manager.redis_client:
info = self.cache_manager.redis_client.info()
return {
'healthy': True,
'connected_clients': info.get('connected_clients', 0),
'used_memory': info.get('used_memory_human', '0B'),
}
else:
return {'healthy': True, 'note': 'Redis not configured, using default cache'}
except Exception as e:
return {'healthy': False, 'error': str(e)}
def _check_tenant_cache_health(self) -> Dict[str, Any]:
"""Check tenant cache health."""
try:
stats = self.tenant_cache_manager.get_tenant_cache_stats()
return {
'healthy': True,
'active_tenants': len(stats.get('tenants', {})),
'total_tenants': stats.get('total_tenants', 0),
}
except Exception as e:
return {'healthy': False, 'error': str(e)}
def _check_malaysian_cache_health(self) -> Dict[str, Any]:
"""Check Malaysian cache health."""
try:
# Test Malaysian-specific cache operations
test_postcode = '50000'
test_data = {'city': 'Kuala Lumpur', 'state': 'WP Kuala Lumpur'}
success = self.malaysian_cache.set_cached_postcode_data(test_postcode, test_data)
if not success:
return {'healthy': False, 'error': 'Malaysian cache set failed'}
retrieved = self.malaysian_cache.get_cached_postcode_data(test_postcode)
if retrieved != test_data:
return {'healthy': False, 'error': 'Malaysian cache get failed'}
return {'healthy': True}
except Exception as e:
return {'healthy': False, 'error': str(e)}
def _benchmark_read_operations(self) -> Dict[str, Any]:
"""Benchmark read operations."""
import time
start_time = time.time()
for i in range(1000):
self.cache_manager.get(f'benchmark_key_{i % 100}')
end_time = time.time()
return {
'operations': 1000,
'total_time': end_time - start_time,
'avg_time_per_op': (end_time - start_time) / 1000,
'ops_per_second': 1000 / (end_time - start_time),
}
def _benchmark_write_operations(self) -> Dict[str, Any]:
"""Benchmark write operations."""
import time
start_time = time.time()
for i in range(1000):
self.cache_manager.set(f'benchmark_key_{i}', f'benchmark_value_{i}')
end_time = time.time()
return {
'operations': 1000,
'total_time': end_time - start_time,
'avg_time_per_op': (end_time - start_time) / 1000,
'ops_per_second': 1000 / (end_time - start_time),
}
def _benchmark_malaysian_cache(self) -> Dict[str, Any]:
"""Benchmark Malaysian cache operations."""
import time
start_time = time.time()
for i in range(100):
postcode = str(50000 + i)
self.malaysian_cache.set_cached_postcode_data(
postcode, {'city': 'Test City', 'state': 'Test State'}
)
end_time = time.time()
return {
'operations': 100,
'total_time': end_time - start_time,
'avg_time_per_op': (end_time - start_time) / 100,
'ops_per_second': 100 / (end_time - start_time),
}
def _benchmark_multi_tenant_cache(self) -> Dict[str, Any]:
"""Benchmark multi-tenant cache operations."""
import time
start_time = time.time()
for tenant_id in range(1, 11): # 10 tenants
tenant_cache = self.tenant_cache_manager.get_cache_manager(tenant_id)
for i in range(100):
tenant_cache.set(f'tenant_key_{i}', f'tenant_value_{i}')
end_time = time.time()
return {
'operations': 1000,
'total_time': end_time - start_time,
'avg_time_per_op': (end_time - start_time) / 1000,
'ops_per_second': 1000 / (end_time - start_time),
}
def _output_results(self, results: Dict[str, Any], title: str):
"""Output results in specified format."""
if self.output_format == 'json':
self.stdout.write(json.dumps(results, indent=2, default=str))
elif self.output_format == 'summary':
self._output_summary(results, title)
else:
self._output_table(results, title)
def _output_summary(self, results: Dict[str, Any], title: str):
"""Output summary format."""
self.stdout.write(f"\n{title}")
self.stdout.write("=" * len(title))
for key, value in results.items():
if isinstance(value, dict):
self.stdout.write(f"{key}:")
for sub_key, sub_value in value.items():
self.stdout.write(f" {sub_key}: {sub_value}")
else:
self.stdout.write(f"{key}: {value}")
def _output_table(self, results: Dict[str, Any], title: str):
"""Output table format."""
self.stdout.write(f"\n{title}")
self.stdout.write("=" * len(title))
# Simple table output - in production you might use tabulate or similar
for key, value in results.items():
if isinstance(value, dict):
self.stdout.write(f"\n{key}:")
for sub_key, sub_value in value.items():
self.stdout.write(f" {sub_key:<20} {sub_value}")
else:
self.stdout.write(f"{key:<20} {value}")
if self.verbose:
self.stdout.write("\nVerbose output enabled")
# Add additional verbose information here

View File

@@ -0,0 +1,554 @@
"""
Database Optimization Management Command
This management command provides comprehensive database optimization utilities
for the multi-tenant SaaS platform, including index management, query optimization,
performance analysis, and maintenance operations specifically designed for
Malaysian deployment scenarios.
"""
import argparse
import json
import logging
import sys
from typing import List, Dict, Any, Optional
from django.core.management.base import BaseCommand, CommandError
from django.db import connection
from django.core.cache import cache
from django.conf import settings
from django.utils import timezone
from django_tenants.utils import get_tenant_model, schema_context
from core.optimization.query_optimization import (
DatabaseOptimizer,
QueryOptimizer,
CacheManager,
DatabaseMaintenance
)
from core.optimization.index_manager import (
IndexManager,
IndexType,
IndexStatus
)
from core.optimization.config import (
get_config,
DatabaseConfig,
validate_environment_config
)
logger = logging.getLogger(__name__)
class Command(BaseCommand):
"""
Database optimization management command.
Usage:
python manage.py optimize_database <action> [options]
Actions:
analyze - Analyze database performance
indexes - Manage database indexes
queries - Optimize database queries
cache - Manage database cache
maintenance - Perform database maintenance
config - Show configuration
malaysian - Malaysian-specific optimizations
report - Generate comprehensive report
"""
help = 'Optimize database performance for the multi-tenant SaaS platform'
def add_arguments(self, parser):
"""Add command arguments."""
parser.add_argument(
'action',
choices=[
'analyze', 'indexes', 'queries', 'cache',
'maintenance', 'config', 'malaysian', 'report'
],
help='Optimization action to perform'
)
parser.add_argument(
'--tenant',
help='Specific tenant schema to optimize'
)
parser.add_argument(
'--environment',
choices=['production', 'staging', 'development'],
default='production',
help='Environment configuration to use'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Show what would be done without executing'
)
parser.add_argument(
'--verbose',
action='store_true',
help='Enable verbose output'
)
parser.add_argument(
'--output',
choices=['json', 'table', 'summary'],
default='table',
help='Output format'
)
parser.add_argument(
'--hours',
type=int,
default=24,
help='Number of hours to analyze (default: 24)'
)
parser.add_argument(
'--index-action',
choices=['create', 'drop', 'rebuild', 'analyze'],
help='Specific index action to perform'
)
parser.add_argument(
'--cache-action',
choices=['clear', 'stats', 'warmup'],
help='Cache management action'
)
def handle(self, *args, **options):
"""Handle the command."""
self.setup_logging(options.get('verbose'))
action = options['action']
tenant_schema = options.get('tenant')
environment = options.get('environment')
dry_run = options.get('dry_run')
output_format = options.get('output')
# Validate configuration
if not validate_environment_config(environment):
raise CommandError(f"Invalid configuration for environment: {environment}")
# Get configuration
config = get_config(environment)
if dry_run:
self.stdout.write(
self.style.WARNING(f"DRY RUN MODE - No changes will be made")
)
try:
if action == 'analyze':
self.analyze_database(config, tenant_schema, options, output_format)
elif action == 'indexes':
self.manage_indexes(config, tenant_schema, options, output_format)
elif action == 'queries':
self.optimize_queries(config, tenant_schema, options, output_format)
elif action == 'cache':
self.manage_cache(config, tenant_schema, options, output_format)
elif action == 'maintenance':
self.perform_maintenance(config, tenant_schema, options, output_format)
elif action == 'config':
self.show_configuration(config, output_format)
elif action == 'malaysian':
self.optimize_malaysian(config, tenant_schema, options, output_format)
elif action == 'report':
self.generate_report(config, tenant_schema, options, output_format)
else:
raise CommandError(f"Unknown action: {action}")
except Exception as e:
logger.error(f"Error during optimization: {e}")
raise CommandError(f"Optimization failed: {e}")
def setup_logging(self, verbose: bool):
"""Setup logging configuration."""
level = logging.DEBUG if verbose else logging.INFO
logging.basicConfig(
level=level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
def analyze_database(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Analyze database performance."""
self.stdout.write("Analyzing database performance...")
optimizer = DatabaseOptimizer(tenant_schema)
# Analyze query performance
hours = options.get('hours', 24)
performance_analysis = optimizer.analyze_query_performance(hours)
# Analyze indexes
index_manager = IndexManager(tenant_schema)
index_performance = index_manager.analyze_index_performance()
# Get table statistics
table_stats = DatabaseMaintenance.get_table_sizes()
# Combine results
analysis_results = {
'performance_analysis': performance_analysis,
'index_analysis': index_performance,
'table_statistics': table_stats,
'optimization_recommendations': optimizer.get_optimization_report()
}
self.output_results(analysis_results, output_format)
def manage_indexes(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Manage database indexes."""
index_action = options.get('index_action')
dry_run = options.get('dry_run')
index_manager = IndexManager(tenant_schema)
if index_action == 'analyze':
self.stdout.write("Analyzing indexes...")
results = index_manager.analyze_index_performance()
self.output_results(results, output_format)
elif index_action == 'create':
self.stdout.write("Creating Malaysian-specific indexes...")
created = index_manager.create_malaysian_indexes()
created.extend(index_manager.create_multi_tenant_indexes())
if dry_run:
self.stdout.write(f"Would create {len(created)} indexes")
else:
self.stdout.write(
self.style.SUCCESS(f"Created {len(created)} indexes")
)
elif index_action == 'drop':
self.stdout.write("Analyzing unused indexes...")
performance_analysis = index_manager.analyze_index_performance()
unused_recommendations = [
r for r in performance_analysis['recommendations']
if r.action == 'drop'
]
if dry_run:
self.stdout.write(f"Would drop {len(unused_recommendations)} unused indexes")
else:
results = index_manager.execute_recommendations(
unused_recommendations, dry_run
)
self.stdout.write(
self.style.SUCCESS(f"Dropped {results['executed']} indexes")
)
elif index_action == 'rebuild':
self.stdout.write("Rebuilding fragmented indexes...")
performance_analysis = index_manager.analyze_index_performance()
rebuild_recommendations = [
r for r in performance_analysis['recommendations']
if r.action == 'rebuild'
]
if dry_run:
self.stdout.write(f"Would rebuild {len(rebuild_recommendations)} indexes")
else:
results = index_manager.execute_recommendations(
rebuild_recommendations, dry_run
)
self.stdout.write(
self.style.SUCCESS(f"Rebuilt {results['executed']} indexes")
)
else:
# Show index statistics
stats = index_manager.get_index_statistics()
self.output_results(stats, output_format)
def optimize_queries(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Optimize database queries."""
self.stdout.write("Optimizing database queries...")
optimizer = DatabaseOptimizer(tenant_schema)
# Get optimization report
report = optimizer.get_optimization_report()
# Optimize Malaysian queries
malaysian_opts = optimizer.optimize_malaysian_queries()
# Add to report
report['malaysian_optimizations'] = malaysian_opts
self.output_results(report, output_format)
def manage_cache(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Manage database cache."""
cache_action = options.get('cache_action')
cache_manager = CacheManager()
if cache_action == 'clear':
self.stdout.write("Clearing cache...")
if options.get('dry_run'):
self.stdout.write("Would clear all cache")
else:
cache.clear()
self.stdout.write(
self.style.SUCCESS("Cache cleared successfully")
)
elif cache_action == 'stats':
self.stdout.write("Getting cache statistics...")
try:
# Get Redis stats if using Redis
if 'redis' in str(config.cache.backend):
import redis
r = redis.from_url(config.cache.location)
stats = r.info()
self.output_results(stats, output_format)
else:
self.stdout.write("Cache statistics not available for current backend")
except Exception as e:
self.stdout.write(
self.style.ERROR(f"Error getting cache stats: {e}")
)
elif cache_action == 'warmup':
self.stdout.write("Warming up cache...")
# Implement cache warmup logic here
self.stdout.write("Cache warmup completed")
else:
# Show cache configuration
cache_config = {
'backend': config.cache.backend.value,
'location': config.cache.location,
'timeout': config.cache.timeout,
'key_prefix': config.cache.key_prefix,
'enabled': config.performance.enable_caching
}
self.output_results(cache_config, output_format)
def perform_maintenance(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Perform database maintenance."""
self.stdout.write("Performing database maintenance...")
maintenance = DatabaseMaintenance()
# Run maintenance tasks
with connection.cursor() as cursor:
# Analyze tables
cursor.execute("ANALYZE VERBOSE")
self.stdout.write("Analyzed database tables")
# Update statistics
cursor.execute("VACUUM ANALYZE")
self.stdout.write("Vacuumed and analyzed database")
# Get maintenance results
results = {
'tables_analyzed': len(DatabaseMaintenance.get_table_sizes()),
'maintenance_completed': timezone.now(),
'next_recommended': timezone.now() + timezone.timedelta(days=7)
}
self.output_results(results, output_format)
def show_configuration(self, config: DatabaseConfig, output_format: str):
"""Show current database configuration."""
self.stdout.write("Database Configuration:")
# Get all configuration settings
db_config = config.get_database_optimization_settings()
# Add Django settings
db_config['django_database'] = config.get_django_database_config()
db_config['django_cache'] = config.get_django_cache_config()
# Add validation warnings
warnings = config.validate_configuration()
if warnings:
db_config['warnings'] = warnings
# Add recommendations
recommendations = config.get_performance_recommendations()
if recommendations:
db_config['recommendations'] = recommendations
self.output_results(db_config, output_format)
def optimize_malaysian(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Perform Malaysian-specific optimizations."""
self.stdout.write("Performing Malaysian-specific optimizations...")
optimizer = DatabaseOptimizer(tenant_schema)
index_manager = IndexManager(tenant_schema)
# Create Malaysian indexes
created_indexes = index_manager.create_malaysian_indexes()
# Optimize Malaysian queries
malaysian_opts = optimizer.optimize_malaysian_queries()
# Get Malaysian-specific configuration
malaysian_config = {
'indexes_created': len(created_indexes),
'index_names': created_indexes,
'sst_queries_optimized': malaysian_opts['sst_queries_optimized'],
'ic_validation_optimized': malaysian_opts['ic_validation_optimized'],
'address_queries_optimized': malaysian_opts['address_queries_optimized'],
'localization_improvements': malaysian_opts['localization_improvements'],
'malaysian_config': {
'timezone': config.malaysian.timezone,
'locale': config.malaysian.locale,
'currency': config.malaysian.currency,
'local_caching_enabled': config.malaysian.enable_local_caching
}
}
self.output_results(malaysian_config, output_format)
def generate_report(self, config: DatabaseConfig, tenant_schema: Optional[str],
options: Dict[str, Any], output_format: str):
"""Generate comprehensive optimization report."""
self.stdout.write("Generating comprehensive optimization report...")
optimizer = DatabaseOptimizer(tenant_schema)
index_manager = IndexManager(tenant_schema)
# Collect all data for report
report_data = {
'report_generated': timezone.now(),
'environment': config.environment,
'tenant_schema': tenant_schema,
'configuration': config.get_database_optimization_settings(),
'performance_analysis': optimizer.analyze_query_performance(),
'index_analysis': index_manager.analyze_index_performance(),
'index_statistics': index_manager.get_index_statistics(),
'optimization_report': optimizer.get_optimization_report(),
'table_statistics': DatabaseMaintenance.get_table_sizes(),
'malaysian_optimizations': optimizer.optimize_malaysian_queries(),
'configuration_validation': config.validate_configuration(),
'recommendations': config.get_performance_recommendations()
}
self.output_results(report_data, output_format)
def output_results(self, results: Dict[str, Any], output_format: str):
"""Output results in specified format."""
if output_format == 'json':
self.output_json(results)
elif output_format == 'table':
self.output_table(results)
elif output_format == 'summary':
self.output_summary(results)
else:
self.output_table(results)
def output_json(self, results: Dict[str, Any]):
"""Output results as JSON."""
# Convert datetime objects to strings
def json_serializer(obj):
if hasattr(obj, 'isoformat'):
return obj.isoformat()
elif hasattr(obj, 'value'):
return obj.value
elif hasattr(obj, '__dict__'):
return obj.__dict__
return str(obj)
json_output = json.dumps(results, indent=2, default=json_serializer)
self.stdout.write(json_output)
def output_table(self, results: Dict[str, Any]):
"""Output results as formatted tables."""
for key, value in results.items():
self.stdout.write(f"\n{self.style.SUCCESS(key.upper()}:}")
if isinstance(value, dict):
for sub_key, sub_value in value.items():
self.stdout.write(f" {sub_key}: {sub_value}")
elif isinstance(value, list):
for i, item in enumerate(value):
self.stdout.write(f" {i+1}. {item}")
else:
self.stdout.write(f" {value}")
def output_summary(self, results: Dict[str, Any]):
"""Output results as summary."""
self.stdout.write(self.style.SUCCESS("OPTIMIZATION SUMMARY:"))
# Extract key metrics
total_queries = results.get('performance_analysis', {}).get('total_queries', 0)
slow_queries = results.get('performance_analysis', {}).get('slow_queries', 0)
total_indexes = results.get('index_analysis', {}).get('total_indexes', 0)
unused_indexes = results.get('index_analysis', {}).get('unused_indexes', 0)
recommendations = results.get('index_analysis', {}).get('recommendations', [])
self.stdout.write(f"• Total queries analyzed: {total_queries}")
self.stdout.write(f"• Slow queries found: {slow_queries}")
self.stdout.write(f"• Total indexes: {total_indexes}")
self.stdout.write(f"• Unused indexes: {unused_indexes}")
self.stdout.write(f"• Recommendations: {len(recommendations)}")
if recommendations:
self.stdout.write("\nTOP RECOMMENDATIONS:")
for i, rec in enumerate(recommendations[:5]):
priority = rec.get('priority', 'medium')
action = rec.get('action', 'unknown')
reason = rec.get('reason', 'No reason provided')
self.stdout.write(f" {i+1}. [{priority.upper()}] {action}: {reason}")
# Malaysian-specific summary
malaysian_opts = results.get('malaysian_optimizations', {})
if malaysian_opts:
self.stdout.write(f"\nMALAYSIAN OPTIMIZATIONS:")
self.stdout.write(f"• SST queries optimized: {malaysian_opts.get('sst_queries_optimized', 0)}")
self.stdout.write(f"• IC validation optimized: {malaysian_opts.get('ic_validation_optimized', False)}")
self.stdout.write(f"• Address queries optimized: {malaysian_opts.get('address_queries_optimized', 0)}")
def create_progress_bar(self, total: int, description: str):
"""Create a simple progress bar."""
return ProgressBar(total, description)
class ProgressBar:
"""Simple progress bar for command line output."""
def __init__(self, total: int, description: str):
self.total = total
self.current = 0
self.description = description
def update(self, increment: int = 1):
"""Update progress."""
self.current += increment
self._draw()
def _draw(self):
"""Draw progress bar."""
if self.total == 0:
return
progress = self.current / self.total
bar_length = 50
filled = int(bar_length * progress)
bar = '' * filled + '-' * (bar_length - filled)
percent = progress * 100
self.stdout.write(f"\r{self.description}: |{bar}| {percent:.1f}% ({self.current}/{self.total})")
self.stdout.flush()
def finish(self):
"""Finish progress bar."""
self._draw()
self.stdout.write("\n")
self.stdout.flush()

View File

@@ -0,0 +1,627 @@
"""
Database Configuration Optimization
This module provides optimized database configuration settings for the multi-tenant SaaS platform,
including connection pooling, query optimization, caching strategies, and performance tuning
specifically designed for Malaysian deployment scenarios.
"""
import os
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
from enum import Enum
class DatabaseEngine(Enum):
"""Supported database engines."""
POSTGRESQL = "postgresql"
MYSQL = "mysql"
SQLITE = "sqlite3"
class CacheBackend(Enum):
"""Supported cache backends."""
REDIS = "redis"
MEMCACHED = "memcached"
DATABASE = "database"
DUMMY = "dummy"
@dataclass
class ConnectionPoolConfig:
"""Configuration for database connection pooling."""
max_connections: int = 100
min_connections: int = 2
connect_timeout: int = 10
idle_timeout: int = 300
max_lifetime: int = 3600
reuse_timeout: int = 30
health_check_interval: int = 60
health_check_timeout: int = 5
@dataclass
class QueryOptimizationConfig:
"""Configuration for query optimization."""
slow_query_threshold: float = 1.0 # seconds
query_cache_timeout: int = 3600 # seconds
enable_query_logging: bool = True
max_query_length: int = 10000
force_index_hints: bool = False
optimize_joins: bool = True
batch_size: int = 1000
@dataclass
class CacheConfig:
"""Configuration for caching."""
backend: CacheBackend = CacheBackend.REDIS
location: str = "redis://127.0.0.1:6379/1"
timeout: int = 300
key_prefix: str = "saas_"
version: int = 1
options: Dict[str, Any] = None
def __post_init__(self):
if self.options is None:
self.options = {}
@dataclass
class MultiTenantConfig:
"""Configuration for multi-tenant database optimization."""
shared_tables: List[str] = None
tenant_table_prefix: str = "tenant_"
enable_tenant_caching: bool = True
tenant_cache_timeout: int = 1800
enable_cross_tenant_queries: bool = False
tenant_isolation_level: str = "strict"
def __post_init__(self):
if self.shared_tables is None:
self.shared_tables = [
"public.tenant",
"public.django_migrations",
"public.django_content_type",
"public.django_admin_log"
]
@dataclass
class MalaysianConfig:
"""Configuration specific to Malaysian deployment."""
timezone: str = "Asia/Kuala_Lumpur"
locale: str = "ms_MY"
currency: str = "MYR"
enable_local_caching: bool = True
local_cache_timeout: int = 900
malaysian_indexes_enabled: bool = True
sst_calculation_cache: bool = True
ic_validation_cache: bool = True
address_optimization: bool = True
@dataclass
class PerformanceConfig:
"""General performance configuration."""
enable_connection_pooling: bool = True
enable_query_optimization: bool = True
enable_caching: bool = True
enable_monitoring: bool = True
log_slow_queries: bool = True
enable_query_profiling: bool = False
enable_database_maintenance: bool = True
class DatabaseConfig:
"""
Centralized database configuration management for the multi-tenant SaaS platform.
This class provides optimized configuration settings for different deployment scenarios
with specific optimizations for Malaysian market requirements.
"""
def __init__(self, environment: str = "production"):
self.environment = environment
self.connection_pool = self._get_connection_pool_config()
self.query_optimization = self._get_query_optimization_config()
self.cache = self._get_cache_config()
self.multi_tenant = self._get_multi_tenant_config()
self.malaysian = self._get_malaysian_config()
self.performance = self._get_performance_config()
def _get_connection_pool_config(self) -> ConnectionPoolConfig:
"""Get connection pool configuration based on environment."""
if self.environment == "production":
return ConnectionPoolConfig(
max_connections=200,
min_connections=10,
connect_timeout=10,
idle_timeout=600,
max_lifetime=7200,
reuse_timeout=60,
health_check_interval=120,
health_check_timeout=10
)
elif self.environment == "staging":
return ConnectionPoolConfig(
max_connections=100,
min_connections=5,
connect_timeout=15,
idle_timeout=300,
max_lifetime=3600,
reuse_timeout=30,
health_check_interval=60,
health_check_timeout=5
)
else: # development
return ConnectionPoolConfig(
max_connections=50,
min_connections=2,
connect_timeout=5,
idle_timeout=60,
max_lifetime=1800,
reuse_timeout=15,
health_check_interval=30,
health_check_timeout=3
)
def _get_query_optimization_config(self) -> QueryOptimizationConfig:
"""Get query optimization configuration based on environment."""
if self.environment == "production":
return QueryOptimizationConfig(
slow_query_threshold=0.5,
query_cache_timeout=7200,
enable_query_logging=True,
max_query_length=50000,
force_index_hints=True,
optimize_joins=True,
batch_size=2000
)
elif self.environment == "staging":
return QueryOptimizationConfig(
slow_query_threshold=1.0,
query_cache_timeout=3600,
enable_query_logging=True,
max_query_length=10000,
force_index_hints=False,
optimize_joins=True,
batch_size=1000
)
else: # development
return QueryOptimizationConfig(
slow_query_threshold=2.0,
query_cache_timeout=1800,
enable_query_logging=False,
max_query_length=10000,
force_index_hints=False,
optimize_joins=False,
batch_size=500
)
def _get_cache_config(self) -> CacheConfig:
"""Get cache configuration based on environment."""
if self.environment == "production":
return CacheConfig(
backend=CacheBackend.REDIS,
location=os.getenv("REDIS_URL", "redis://127.0.0.1:6379/1"),
timeout=3600,
key_prefix="saas_prod_",
version=1,
options={
"CLIENT_KWARGS": {
"socket_connect_timeout": 5,
"socket_timeout": 5,
"retry_on_timeout": True
}
}
)
elif self.environment == "staging":
return CacheConfig(
backend=CacheBackend.REDIS,
location=os.getenv("REDIS_URL", "redis://127.0.0.1:6379/2"),
timeout=1800,
key_prefix="saas_staging_",
version=1,
options={
"CLIENT_KWARGS": {
"socket_connect_timeout": 10,
"socket_timeout": 10
}
}
)
else: # development
return CacheConfig(
backend=CacheBackend.DUMMY,
location="",
timeout=300,
key_prefix="saas_dev_",
version=1,
options={}
)
def _get_multi_tenant_config(self) -> MultiTenantConfig:
"""Get multi-tenant configuration based on environment."""
shared_tables = [
"public.tenant",
"public.django_migrations",
"public.django_content_type",
"public.django_admin_log",
"public.django_session"
]
if self.environment == "production":
return MultiTenantConfig(
shared_tables=shared_tables,
tenant_table_prefix="tenant_",
enable_tenant_caching=True,
tenant_cache_timeout=1800,
enable_cross_tenant_queries=False,
tenant_isolation_level="strict"
)
else:
return MultiTenantConfig(
shared_tables=shared_tables,
tenant_table_prefix="tenant_",
enable_tenant_caching=True,
tenant_cache_timeout=900,
enable_cross_tenant_queries=True,
tenant_isolation_level="moderate"
)
def _get_malaysian_config(self) -> MalaysianConfig:
"""Get Malaysian-specific configuration."""
return MalaysianConfig(
timezone="Asia/Kuala_Lumpur",
locale="ms_MY",
currency="MYR",
enable_local_caching=True,
local_cache_timeout=900,
malaysian_indexes_enabled=True,
sst_calculation_cache=True,
ic_validation_cache=True,
address_optimization=True
)
def _get_performance_config(self) -> PerformanceConfig:
"""Get general performance configuration."""
if self.environment == "production":
return PerformanceConfig(
enable_connection_pooling=True,
enable_query_optimization=True,
enable_caching=True,
enable_monitoring=True,
log_slow_queries=True,
enable_query_profiling=True,
enable_database_maintenance=True
)
elif self.environment == "staging":
return PerformanceConfig(
enable_connection_pooling=True,
enable_query_optimization=True,
enable_caching=True,
enable_monitoring=True,
log_slow_queries=True,
enable_query_profiling=False,
enable_database_maintenance=True
)
else: # development
return PerformanceConfig(
enable_connection_pooling=False,
enable_query_optimization=False,
enable_caching=False,
enable_monitoring=False,
log_slow_queries=False,
enable_query_profiling=False,
enable_database_maintenance=False
)
def get_django_database_config(self) -> Dict[str, Any]:
"""
Get Django database configuration dictionary.
Returns:
Dictionary suitable for Django DATABASES setting
"""
base_config = {
"ENGINE": "django_tenants.postgresql_backend",
"NAME": os.getenv("DB_NAME", "saas_platform"),
"USER": os.getenv("DB_USER", "postgres"),
"PASSWORD": os.getenv("DB_PASSWORD", ""),
"HOST": os.getenv("DB_HOST", "localhost"),
"PORT": os.getenv("DB_PORT", "5432"),
"CONN_MAX_AGE": self.connection_pool.max_lifetime,
"OPTIONS": {
"connect_timeout": self.connection_pool.connect_timeout,
"application_name": f"saas_platform_{self.environment}",
"tcp_user_timeout": 10000,
"statement_timeout": 30000,
"idle_in_transaction_session_timeout": 60000,
}
}
# Add connection pooling options if enabled
if self.performance.enable_connection_pooling:
base_config["OPTIONS"].update({
"MAX_CONNS": self.connection_pool.max_connections,
"MIN_CONNS": self.connection_pool.min_connections,
"REUSE_CONNS": self.connection_pool.reuse_timeout,
"IDLE_TIMEOUT": self.connection_pool.idle_timeout,
})
return {
"default": base_config
}
def get_django_cache_config(self) -> Dict[str, Any]:
"""
Get Django cache configuration dictionary.
Returns:
Dictionary suitable for Django CACHES setting
"""
if not self.performance.enable_caching:
return {
"default": {
"BACKEND": "django.core.cache.backends.dummy.DummyCache"
}
}
if self.cache.backend == CacheBackend.REDIS:
return {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": self.cache.location,
"TIMEOUT": self.cache.timeout,
"KEY_PREFIX": self.cache.key_prefix,
"VERSION": self.cache.version,
"OPTIONS": self.cache.options
},
"tenant_cache": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": self.cache.location.replace("/1", "/2"),
"TIMEOUT": self.multi_tenant.tenant_cache_timeout,
"KEY_PREFIX": "tenant_",
"VERSION": 1,
"OPTIONS": self.cache.options
},
"malaysian_cache": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": self.cache.location.replace("/1", "/3"),
"TIMEOUT": self.malaysian.local_cache_timeout,
"KEY_PREFIX": "malaysian_",
"VERSION": 1,
"OPTIONS": self.cache.options
}
}
elif self.cache.backend == CacheBackend.MEMCACHED:
return {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": self.cache.location,
"TIMEOUT": self.cache.timeout,
"KEY_PREFIX": self.cache.key_prefix,
"VERSION": self.cache.version
}
}
else:
return {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": "cache_table",
"TIMEOUT": self.cache.timeout,
"KEY_PREFIX": self.cache.key_prefix,
"VERSION": self.cache.version
}
}
def get_database_optimization_settings(self) -> Dict[str, Any]:
"""
Get database optimization settings.
Returns:
Dictionary with optimization settings
"""
return {
"connection_pool": asdict(self.connection_pool),
"query_optimization": asdict(self.query_optimization),
"cache": asdict(self.cache),
"multi_tenant": asdict(self.multi_tenant),
"malaysian": asdict(self.malaysian),
"performance": asdict(self.performance)
}
def get_postgresql_settings(self) -> List[str]:
"""
Get PostgreSQL configuration settings.
Returns:
List of PostgreSQL configuration commands
"""
settings = []
if self.environment == "production":
settings.extend([
"ALTER SYSTEM SET shared_buffers = '256MB'",
"ALTER SYSTEM SET effective_cache_size = '1GB'",
"ALTER SYSTEM SET maintenance_work_mem = '64MB'",
"ALTER SYSTEM SET checkpoint_completion_target = 0.9",
"ALTER SYSTEM SET wal_buffers = '16MB'",
"ALTER SYSTEM SET default_statistics_target = 100",
"ALTER SYSTEM SET random_page_cost = 1.1",
"ALTER SYSTEM SET effective_io_concurrency = 200",
"ALTER SYSTEM SET work_mem = '4MB'",
"ALTER SYSTEM SET min_wal_size = '1GB'",
"ALTER SYSTEM SET max_wal_size = '4GB'",
"ALTER SYSTEM SET max_worker_processes = 8",
"ALTER SYSTEM SET max_parallel_workers_per_gather = 4",
"ALTER SYSTEM SET max_parallel_workers = 8",
"ALTER SYSTEM SET max_parallel_maintenance_workers = 4",
"ALTER SYSTEM SET log_statement = 'mod'",
"ALTER SYSTEM SET log_min_duration_statement = '500'",
"ALTER SYSTEM SET log_checkpoints = 'on'",
"ALTER SYSTEM SET log_connections = 'on'",
"ALTER SYSTEM SET log_disconnections = 'on'",
"ALTER SYSTEM SET log_lock_waits = 'on'",
"ALTER SYSTEM SET log_temp_files = '0'",
"ALTER SYSTEM SET log_timezone = 'Asia/Kuala_Lumpur'",
"ALTER SYSTEM SET timezone = 'Asia/Kuala_Lumpur'",
])
elif self.environment == "staging":
settings.extend([
"ALTER SYSTEM SET shared_buffers = '128MB'",
"ALTER SYSTEM SET effective_cache_size = '512MB'",
"ALTER SYSTEM SET maintenance_work_mem = '32MB'",
"ALTER SYSTEM SET checkpoint_completion_target = 0.7",
"ALTER SYSTEM SET default_statistics_target = 50",
"ALTER SYSTEM SET work_mem = '2MB'",
"ALTER SYSTEM SET log_min_duration_statement = '1000'",
"ALTER SYSTEM SET log_timezone = 'Asia/Kuala_Lumpur'",
"ALTER SYSTEM SET timezone = 'Asia/Kuala_Lumpur'",
])
return settings
def get_environment_overrides(self) -> Dict[str, Any]:
"""
Get environment-specific overrides.
Returns:
Dictionary with environment overrides
"""
env_overrides = os.getenv("DB_CONFIG_OVERRIDES")
if env_overrides:
try:
import json
return json.loads(env_overrides)
except json.JSONDecodeError:
pass
return {}
def validate_configuration(self) -> List[str]:
"""
Validate the current configuration.
Returns:
List of validation warnings or errors
"""
warnings = []
# Check connection pool settings
if self.performance.enable_connection_pooling:
if self.connection_pool.max_connections < 10:
warnings.append("Max connections might be too low for production")
if self.connection_pool.min_connections > self.connection_pool.max_connections // 2:
warnings.append("Min connections should not exceed half of max connections")
# Check cache settings
if self.performance.enable_caching:
if self.cache.backend == CacheBackend.REDIS:
if not self.cache.location.startswith("redis://"):
warnings.append("Redis URL format is incorrect")
# Check query optimization settings
if self.query_optimization.slow_query_threshold < 0.1:
warnings.append("Slow query threshold might be too aggressive")
# Check multi-tenant settings
if not self.multi_tenant.shared_tables:
warnings.append("No shared tables configured for multi-tenant setup")
return warnings
def get_performance_recommendations(self) -> List[str]:
"""
Get performance recommendations based on current configuration.
Returns:
List of performance recommendations
"""
recommendations = []
if self.environment == "production":
if self.connection_pool.max_connections < 100:
recommendations.append("Consider increasing max_connections for better concurrency")
if self.query_optimization.slow_query_threshold > 1.0:
recommendations.append("Consider reducing slow_query_threshold for better monitoring")
if not self.performance.enable_query_profiling:
recommendations.append("Consider enabling query profiling for production optimization")
# Malaysian-specific recommendations
if self.malaysian.enable_local_caching:
recommendations.append("Malaysian local caching enabled - monitor cache hit rates")
if self.malaysian.malaysian_indexes_enabled:
recommendations.append("Ensure Malaysian-specific indexes are created and maintained")
# Multi-tenant recommendations
if self.multi_tenant.enable_tenant_caching:
recommendations.append("Monitor tenant cache hit rates and memory usage")
return recommendations
# Configuration factory functions
def get_config(environment: str = None) -> DatabaseConfig:
"""
Get database configuration for specified environment.
Args:
environment: Environment name (production, staging, development)
Returns:
DatabaseConfig instance
"""
if environment is None:
environment = os.getenv("DJANGO_ENV", "development")
return DatabaseConfig(environment)
def get_production_config() -> DatabaseConfig:
"""Get production database configuration."""
return DatabaseConfig("production")
def get_staging_config() -> DatabaseConfig:
"""Get staging database configuration."""
return DatabaseConfig("staging")
def get_development_config() -> DatabaseConfig:
"""Get development database configuration."""
return DatabaseConfig("development")
# Configuration validation
def validate_environment_config(environment: str) -> bool:
"""
Validate configuration for specified environment.
Args:
environment: Environment name
Returns:
True if configuration is valid
"""
config = get_config(environment)
warnings = config.validate_configuration()
return len(warnings) == 0
# Export classes and functions
__all__ = [
'DatabaseConfig',
'ConnectionPoolConfig',
'QueryOptimizationConfig',
'CacheConfig',
'MultiTenantConfig',
'MalaysianConfig',
'PerformanceConfig',
'DatabaseEngine',
'CacheBackend',
'get_config',
'get_production_config',
'get_staging_config',
'get_development_config',
'validate_environment_config',
]

View File

@@ -0,0 +1,865 @@
"""
Database Index Management Module
This module provides comprehensive index management utilities for the multi-tenant SaaS platform,
including index creation, monitoring, optimization, and maintenance specifically designed for
PostgreSQL with multi-tenant architecture and Malaysian market requirements.
"""
import logging
from typing import Dict, List, Optional, Tuple, Any, Set
from django.db import connection, connections
from django.core.cache import cache
from django.utils import timezone
from django_tenants.utils import schema_context
import time
import json
from dataclasses import dataclass, asdict
from enum import Enum
import re
logger = logging.getLogger(__name__)
class IndexType(Enum):
"""Types of database indexes."""
BTREE = "btree"
HASH = "hash"
GIST = "gist"
GIN = "gin"
BRIN = "brin"
SPGIST = "spgist"
PARTIAL = "partial"
EXPRESSION = "expression"
UNIQUE = "unique"
COMPOSITE = "composite"
class IndexStatus(Enum):
"""Status of database indexes."""
ACTIVE = "active"
INACTIVE = "inactive"
INVALID = "invalid"
CREATING = "creating"
DROPPING = "dropping"
REBUILDING = "rebuilding"
@dataclass
class IndexInfo:
"""Information about a database index."""
name: str
table_name: str
column_names: List[str]
index_type: IndexType
status: IndexStatus
is_unique: bool
is_primary: bool
size_bytes: int
usage_count: int
last_used: Optional[timezone.datetime]
create_statement: str
tenant_schema: str
@dataclass
class IndexRecommendation:
"""Recommendation for index management."""
action: str # 'create', 'drop', 'rebuild', 'modify'
index_name: Optional[str]
table_name: str
columns: List[str]
index_type: IndexType
reason: str
impact: str
priority: str # 'low', 'medium', 'high', 'critical'
estimated_benefit: str
class IndexManager:
"""
Comprehensive index management system for the multi-tenant SaaS platform.
Features:
- Automatic index creation and management
- Performance monitoring and analysis
- Multi-tenant index optimization
- Malaysian market-specific indexing
- Index maintenance and cleanup
"""
def __init__(self, tenant_schema: Optional[str] = None):
self.tenant_schema = tenant_schema
self.index_cache = {}
self.last_analysis = None
self.stats = {
'indexes_managed': 0,
'indexes_created': 0,
'indexes_dropped': 0,
'indexes_rebuilt': 0,
'performance_improvement': 0.0
}
def get_all_indexes(self, refresh: bool = False) -> List[IndexInfo]:
"""
Get all indexes in the database.
Args:
refresh: Force refresh from database
Returns:
List of IndexInfo objects
"""
cache_key = f"all_indexes_{self.tenant_schema or 'public'}"
if not refresh and cache_key in self.index_cache:
return self.index_cache[cache_key]
indexes = []
with connection.cursor() as cursor:
# Get basic index information
cursor.execute("""
SELECT
i.relname as index_name,
t.relname as table_name,
am.amname as index_type,
idx.indisunique as is_unique,
idx.indisprimary as is_primary,
pg_get_indexdef(idx.indexrelid) as definition,
pg_relation_size(i.relid) as size_bytes,
schemaname
FROM pg_index idx
JOIN pg_class i ON i.oid = idx.indexrelid
JOIN pg_class t ON t.oid = idx.indrelid
JOIN pg_namespace n ON n.oid = t.relnamespace
JOIN pg_am am ON am.oid = i.relam
WHERE schemaname = %s
ORDER BY t.relname, i.relname
""", [self.tenant_schema or 'public'])
results = cursor.fetchall()
for row in results:
index_name, table_name, index_type_str, is_unique, is_primary, definition, size_bytes, schema = row
# Extract column names from definition
column_names = self._extract_column_names(definition)
# Get usage statistics
usage_info = self._get_index_usage(cursor, index_name, schema)
# Determine index type
index_type = self._determine_index_type(definition, index_type_str)
# Get index status
status = self._get_index_status(cursor, index_name, schema)
index_info = IndexInfo(
name=index_name,
table_name=table_name,
column_names=column_names,
index_type=index_type,
status=status,
is_unique=is_unique,
is_primary=is_primary,
size_bytes=size_bytes or 0,
usage_count=usage_info.get('usage_count', 0),
last_used=usage_info.get('last_used'),
create_statement=definition,
tenant_schema=schema
)
indexes.append(index_info)
self.index_cache[cache_key] = indexes
self.last_analysis = timezone.now()
return indexes
def _extract_column_names(self, definition: str) -> List[str]:
"""Extract column names from index definition."""
# Extract column names from CREATE INDEX statement
match = re.search(r'ON\s+\w+\s*\(([^)]+)\)', definition)
if match:
columns_part = match.group(1]
# Split by commas and clean up
columns = [col.strip().strip('"') for col in columns_part.split(',')]
return columns
return []
def _get_index_usage(self, cursor, index_name: str, schema: str) -> Dict[str, Any]:
"""Get index usage statistics."""
try:
cursor.execute("""
SELECT
idx_scan as usage_count,
idx_tup_read as tuples_read,
idx_tup_fetch as tuples_fetched
FROM pg_stat_user_indexes
WHERE schemaname = %s AND indexrelname = %s
""", [schema, index_name])
result = cursor.fetchone()
if result:
return {
'usage_count': result[0] or 0,
'tuples_read': result[1] or 0,
'tuples_fetched': result[2] or 0,
'last_used': timezone.now() if result[0] > 0 else None
}
except Exception as e:
logger.error(f"Error getting index usage for {index_name}: {e}")
return {'usage_count': 0, 'tuples_read': 0, 'tuples_fetched': 0}
def _determine_index_type(self, definition: str, am_name: str) -> IndexType:
"""Determine index type from definition and access method."""
if am_name == "btree":
# Check for special cases
if "UNIQUE" in definition.upper():
return IndexType.UNIQUE
elif "WHERE" in definition.upper():
return IndexType.PARTIAL
elif "(" in definition and ")" in definition:
# Check if it's an expression index
content_between_parens = re.search(r'\(([^)]+)\)', definition)
if content_between_parens:
content = content_between_parens.group(1)
if not all(col.strip().isalnum() for col in content.split(',')):
return IndexType.EXPRESSION
return IndexType.BTREE
elif am_name == "hash":
return IndexType.HASH
elif am_name == "gist":
return IndexType.GIST
elif am_name == "gin":
return IndexType.GIN
elif am_name == "brin":
return IndexType.BRIN
elif am_name == "spgist":
return IndexType.SPGIST
return IndexType.BTREE
def _get_index_status(self, cursor, index_name: str, schema: str) -> IndexStatus:
"""Get current status of an index."""
try:
cursor.execute("""
SELECT indisvalid
FROM pg_index
WHERE indexrelid = (
SELECT oid FROM pg_class
WHERE relname = %s AND relnamespace = (
SELECT oid FROM pg_namespace WHERE nspname = %s
)
)
""", [index_name, schema])
result = cursor.fetchone()
if result:
return IndexStatus.ACTIVE if result[0] else IndexStatus.INVALID
except Exception as e:
logger.error(f"Error getting index status for {index_name}: {e}")
return IndexStatus.ACTIVE
def analyze_index_performance(self) -> Dict[str, Any]:
"""
Analyze index performance and generate recommendations.
Returns:
Dictionary with performance analysis and recommendations
"""
indexes = self.get_all_indexes(refresh=True)
recommendations = []
# Analyze unused indexes
unused_indexes = [
idx for idx in indexes
if idx.usage_count == 0 and not idx.is_primary
]
for idx in unused_indexes:
recommendations.append(IndexRecommendation(
action="drop",
index_name=idx.name,
table_name=idx.table_name,
columns=idx.column_names,
index_type=idx.index_type,
reason=f"Index {idx.name} has never been used",
impact="Reduces storage and maintenance overhead",
priority="medium",
estimated_benefit=f"Save {self._format_bytes(idx.size_bytes)}"
))
# Analyze duplicate indexes
recommendations.extend(self._find_duplicate_indexes(indexes))
# Analyze missing indexes
recommendations.extend(self._find_missing_indexes())
# Analyze fragmented indexes
recommendations.extend(self._analyze_fragmentation(indexes))
return {
'total_indexes': len(indexes),
'unused_indexes': len(unused_indexes),
'total_index_size': sum(idx.size_bytes for idx in indexes),
'recommendations': recommendations,
'high_priority_count': len([r for r in recommendations if r.priority == 'critical']),
'analysis_timestamp': timezone.now()
}
def _find_duplicate_indexes(self, indexes: List[IndexInfo]) -> List[IndexRecommendation]:
"""Find duplicate or redundant indexes."""
recommendations = []
index_groups = {}
# Group indexes by table and columns
for idx in indexes:
key = (idx.table_name, tuple(sorted(idx.column_names)))
if key not in index_groups:
index_groups[key] = []
index_groups[key].append(idx)
for (table, columns), group in index_groups.items():
if len(group) > 1:
# Sort by usage and keep the most used
group.sort(key=lambda x: x.usage_count, reverse=True)
keep_idx = group[0]
for drop_idx in group[1:]:
recommendations.append(IndexRecommendation(
action="drop",
index_name=drop_idx.name,
table_name=table,
columns=list(columns),
index_type=drop_idx.index_type,
reason=f"Duplicate index (redundant with {keep_idx.name})",
impact="Reduces storage and write overhead",
priority="low",
estimated_benefit=f"Save {self._format_bytes(drop_idx.size_bytes)}"
))
return recommendations
def _find_missing_indexes(self) -> List[IndexRecommendation]:
"""Find potentially missing indexes based on query patterns."""
recommendations = []
with connection.cursor() as cursor:
# Analyze sequential scans on large tables
cursor.execute("""
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
pg_total_relation_size(schemaname||'.'||tablename) as table_size
FROM pg_stat_user_tables
WHERE seq_scan > 1000
AND pg_total_relation_size(schemaname||'.'||tablename) > 100 * 1024 * 1024
ORDER BY seq_scan DESC
LIMIT 10
""")
for row in cursor.fetchall():
schema, table, seq_scan, seq_tup_read, table_size = row
recommendations.append(IndexRecommendation(
action="create",
index_name=None,
table_name=table,
columns=["id"], # Default recommendation
index_type=IndexType.BTREE,
reason=f"Table {table} has {seq_scan} sequential scans",
impact="Improve query performance for large table",
priority="high",
estimated_benefit=f"Reduce sequential scans by ~{int(seq_scan * 0.8)}"
))
return recommendations
def _analyze_fragmentation(self, indexes: List[IndexInfo]) -> List[IndexRecommendation]:
"""Analyze index fragmentation and recommend rebuilding."""
recommendations = []
with connection.cursor() as cursor:
for idx in indexes:
# Check index bloat (simplified check)
if idx.size_bytes > 10 * 1024 * 1024: # > 10MB
# Large indexes might benefit from rebuilding
if idx.usage_count > 1000: # Heavily used
recommendations.append(IndexRecommendation(
action="rebuild",
index_name=idx.name,
table_name=idx.table_name,
columns=idx.column_names,
index_type=idx.index_type,
reason=f"Large index {idx.name} with high usage may be fragmented",
impact="Improve query performance and reduce storage",
priority="medium",
estimated_benefit="Optimize read performance"
))
return recommendations
def create_index(self, table_name: str, columns: List[str],
index_type: IndexType = IndexType.BTREE,
unique: bool = False,
partial_condition: Optional[str] = None,
concurrently: bool = True) -> str:
"""
Create a new index.
Args:
table_name: Name of the table
columns: List of column names to index
index_type: Type of index to create
unique: Whether to create unique index
partial_condition: WHERE clause for partial index
concurrently: Create index concurrently (locks table less)
Returns:
Name of created index
"""
# Generate index name
index_name = f"idx_{table_name}_{'_'.join(columns)}"
if unique:
index_name = f"unq_{table_name}_{'_'.join(columns)}"
# Build CREATE INDEX statement
sql_parts = ["CREATE"]
if concurrently:
sql_parts.append("CONCURRENTLY")
if unique:
sql_parts.append("UNIQUE")
sql_parts.append("INDEX")
sql_parts.append(index_name)
sql_parts.append("ON")
sql_parts.append(table_name)
# Add USING clause for non-BTREE indexes
if index_type != IndexType.BTREE:
sql_parts.append(f"USING {index_type.value}")
# Add column list
sql_parts.append(f"({', '.join(columns)})")
# Add partial condition if specified
if partial_condition:
sql_parts.append(f"WHERE {partial_condition}")
create_sql = " ".join(sql_parts) + ";"
try:
with connection.cursor() as cursor:
cursor.execute(create_sql)
logger.info(f"Created index {index_name} on {table_name}")
self.stats['indexes_created'] += 1
self.stats['indexes_managed'] += 1
# Clear cache
self.index_cache.clear()
return index_name
except Exception as e:
logger.error(f"Failed to create index {index_name}: {e}")
raise
def drop_index(self, index_name: str, concurrently: bool = True) -> bool:
"""
Drop an existing index.
Args:
index_name: Name of index to drop
concurrently: Drop index concurrently
Returns:
True if successful, False otherwise
"""
try:
with connection.cursor() as cursor:
drop_sql = f"DROP INDEX {'CONCURRENTLY' if concurrently else ''} {index_name};"
cursor.execute(drop_sql)
logger.info(f"Dropped index {index_name}")
self.stats['indexes_dropped'] += 1
self.stats['indexes_managed'] += 1
# Clear cache
self.index_cache.clear()
return True
except Exception as e:
logger.error(f"Failed to drop index {index_name}: {e}")
return False
def rebuild_index(self, index_name: str) -> bool:
"""
Rebuild an existing index (REINDEX).
Args:
index_name: Name of index to rebuild
Returns:
True if successful, False otherwise
"""
try:
with connection.cursor() as cursor:
cursor.execute(f"REINDEX INDEX {index_name};")
logger.info(f"Rebuilt index {index_name}")
self.stats['indexes_rebuilt'] += 1
self.stats['indexes_managed'] += 1
# Clear cache
self.index_cache.clear()
return True
except Exception as e:
logger.error(f"Failed to rebuild index {index_name}: {e}")
return False
def create_malaysian_indexes(self) -> List[str]:
"""
Create indexes specifically for Malaysian market requirements.
Returns:
List of created index names
"""
created_indexes = []
# Malaysian-specific indexes
malaysian_indexes = [
{
'table': 'core_user',
'columns': ['ic_number'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Malaysian IC validation and lookup'
},
{
'table': 'core_address',
'columns': ['postcode'],
'type': IndexType.BTREE,
'reason': 'Malaysian postcode lookups'
},
{
'table': 'core_address',
'columns': ['state'],
'type': IndexType.BTREE,
'reason': 'Malaysian state filtering'
},
{
'table': 'core_business',
'columns': ['registration_number'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Business registration number lookup'
},
{
'table': 'core_sstrate',
'columns': ['rate'],
'type': IndexType.BTREE,
'reason': 'SST rate queries'
},
{
'table': 'retail_product',
'columns': ['barcode'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Product barcode scanning'
},
{
'table': 'healthcare_patient',
'columns': ['ic_number'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Patient IC number lookup'
},
{
'table': 'education_student',
'columns': ['ic_number'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Student IC number lookup'
},
{
'table': 'logistics_vehicle',
'columns': ['registration_number'],
'type': IndexType.BTREE,
'unique': True,
'reason': 'Vehicle registration lookup'
}
]
for index_config in malaysian_indexes:
try:
index_name = self.create_index(
table_name=index_config['table'],
columns=index_config['columns'],
index_type=index_config['type'],
unique=index_config.get('unique', False)
)
created_indexes.append(index_name)
logger.info(f"Created Malaysian index: {index_name} - {index_config['reason']}")
except Exception as e:
logger.warning(f"Failed to create Malaysian index for {index_config['table']}: {e}")
return created_indexes
def create_multi_tenant_indexes(self) -> List[str]:
"""
Create indexes optimized for multi-tenant architecture.
Returns:
List of created index names
"""
created_indexes = []
# Multi-tenant optimization indexes
tenant_indexes = [
{
'table': 'core_user',
'columns': ['tenant_id', 'is_active'],
'type': IndexType.BTREE,
'reason': 'Tenant-scoped user queries with status'
},
{
'table': 'core_transaction',
'columns': ['tenant_id', 'created_at'],
'type': IndexType.BTREE,
'reason': 'Tenant transaction history by date'
},
{
'table': 'core_subscription',
'columns': ['tenant_id', 'status'],
'type': IndexType.BTREE,
'reason': 'Tenant subscription status queries'
},
{
'table': 'core_auditlog',
'columns': ['tenant_id', 'created_at'],
'type': IndexType.BTREE,
'reason': 'Tenant audit log queries'
},
{
'table': 'core_notification',
'columns': ['tenant_id', 'status'],
'type': IndexType.BTREE,
'reason': 'Tenant notification status queries'
}
]
for index_config in tenant_indexes:
try:
index_name = self.create_index(
table_name=index_config['table'],
columns=index_config['columns'],
index_type=index_config['type']
)
created_indexes.append(index_name)
logger.info(f"Created multi-tenant index: {index_name} - {index_config['reason']}")
except Exception as e:
logger.warning(f"Failed to create multi-tenant index for {index_config['table']}: {e}")
return created_indexes
def get_index_statistics(self) -> Dict[str, Any]:
"""
Get comprehensive index statistics.
Returns:
Dictionary with index statistics
"""
indexes = self.get_all_indexes()
stats = {
'total_indexes': len(indexes),
'total_size_bytes': sum(idx.size_bytes for idx in indexes),
'total_size_formatted': self._format_bytes(sum(idx.size_bytes for idx in indexes)),
'index_types': {},
'status_distribution': {},
'unused_count': len([idx for idx in indexes if idx.usage_count == 0]),
'high_usage_count': len([idx for idx in indexes if idx.usage_count > 1000]),
'large_indexes': [idx.name for idx in indexes if idx.size_bytes > 100 * 1024 * 1024], # > 100MB
'management_stats': self.stats.copy()
}
# Count by index type
for idx in indexes:
idx_type = idx.index_type.value
stats['index_types'][idx_type] = stats['index_types'].get(idx_type, 0) + 1
# Count by status
for idx in indexes:
status = idx.status.value
stats['status_distribution'][status] = stats['status_distribution'].get(status, 0) + 1
return stats
def _format_bytes(self, bytes_value: int) -> str:
"""Format bytes to human readable format."""
for unit in ['B', 'KB', 'MB', 'GB', 'TB']:
if bytes_value < 1024.0:
return f"{bytes_value:.2f} {unit}"
bytes_value /= 1024.0
return f"{bytes_value:.2f} PB"
def execute_recommendations(self, recommendations: List[IndexRecommendation],
dry_run: bool = False) -> Dict[str, Any]:
"""
Execute index recommendations.
Args:
recommendations: List of index recommendations
dry_run: If True, only show what would be done
Returns:
Dictionary with execution results
"""
results = {
'executed': 0,
'failed': 0,
'skipped': 0,
'details': []
}
for rec in recommendations:
try:
if dry_run:
results['details'].append(f"[DRY RUN] Would {rec.action} index for {rec.table_name}")
results['skipped'] += 1
continue
if rec.action == "create":
index_name = self.create_index(
table_name=rec.table_name,
columns=rec.columns,
index_type=rec.index_type
)
results['details'].append(f"Created index {index_name}")
results['executed'] += 1
elif rec.action == "drop":
if rec.index_name:
success = self.drop_index(rec.index_name)
if success:
results['details'].append(f"Dropped index {rec.index_name}")
results['executed'] += 1
else:
results['details'].append(f"Failed to drop index {rec.index_name}")
results['failed'] += 1
elif rec.action == "rebuild":
if rec.index_name:
success = self.rebuild_index(rec.index_name)
if success:
results['details'].append(f"Rebuilt index {rec.index_name}")
results['executed'] += 1
else:
results['details'].append(f"Failed to rebuild index {rec.index_name}")
results['failed'] += 1
except Exception as e:
error_msg = f"Failed to execute recommendation for {rec.table_name}: {e}"
results['details'].append(error_msg)
results['failed'] += 1
logger.error(error_msg)
return results
def maintenance_mode(self, actions: List[str]) -> Dict[str, Any]:
"""
Perform index maintenance operations.
Args:
actions: List of maintenance actions to perform
Returns:
Dictionary with maintenance results
"""
results = {
'actions_completed': 0,
'errors': [],
'summary': {}
}
for action in actions:
try:
if action == "analyze":
self._run_analyze()
results['summary']['analyze'] = "Completed"
elif action == "reindex_all":
self._reindex_all()
results['summary']['reindex_all'] = "Completed"
elif action == "cleanup_unused":
unused_count = self._cleanup_unused_indexes()
results['summary']['cleanup_unused'] = f"Removed {unused_count} unused indexes"
elif action == "update_stats":
self._update_statistics()
results['summary']['update_stats'] = "Completed"
results['actions_completed'] += 1
except Exception as e:
error_msg = f"Failed to perform {action}: {e}"
results['errors'].append(error_msg)
logger.error(error_msg)
return results
def _run_analyze(self):
"""Run ANALYZE on all tables."""
with connection.cursor() as cursor:
cursor.execute("ANALYZE VERBOSE")
logger.info("Database analyze completed")
def _reindex_all(self):
"""Reindex all indexes in the database."""
with connection.cursor() as cursor:
cursor.execute("REINDEX DATABASE")
logger.info("Database reindex completed")
def _cleanup_unused_indexes(self) -> int:
"""Remove unused indexes."""
performance_analysis = self.analyze_index_performance()
unused_recommendations = [r for r in performance_analysis['recommendations']
if r.action == "drop"]
if unused_recommendations:
results = self.execute_recommendations(unused_recommendations)
return len([r for r in results['details'] if "Dropped" in r])
return 0
def _update_statistics(self):
"""Update database statistics."""
with connection.cursor() as cursor:
cursor.execute("VACUUM ANALYZE")
logger.info("Database statistics updated")
# Export main classes and functions
__all__ = [
'IndexManager',
'IndexType',
'IndexStatus',
'IndexInfo',
'IndexRecommendation',
]

View File

@@ -0,0 +1,775 @@
"""
Database Query Optimization Module
This module provides comprehensive database optimization strategies for the multi-tenant SaaS platform,
including query optimization, indexing strategies, and performance monitoring specifically tailored
for Malaysian market requirements and multi-tenant architecture.
"""
import logging
from typing import Dict, List, Optional, Tuple, Any
from django.db import connection, connections, models
from django.db.models import Q, F, ExpressionWrapper, FloatField
from django.db.models.functions import Cast, Coalesce, Lower, Upper
from django.core.cache import cache
from django.conf import settings
from django.utils import timezone
from django_tenants.utils import get_tenant_model, schema_context
import time
import json
from contextlib import contextmanager
from dataclasses import dataclass
from enum import Enum
logger = logging.getLogger(__name__)
class OptimizationLevel(Enum):
"""Optimization levels for different query types."""
BASIC = "basic"
INTERMEDIATE = "intermediate"
ADVANCED = "advanced"
CRITICAL = "critical"
@dataclass
class QueryMetrics:
"""Metrics for tracking query performance."""
query_text: str
execution_time: float
rows_affected: int
index_used: Optional[str]
table_name: str
timestamp: timezone.datetime
optimization_level: OptimizationLevel
tenant_schema: str
@dataclass
class IndexRecommendation:
"""Recommendation for database index creation."""
table_name: str
column_names: List[str]
index_type: str
expected_impact: str
priority: str
query_patterns: List[str]
class DatabaseOptimizer:
"""
Main database optimization class for the multi-tenant SaaS platform.
This class provides comprehensive optimization strategies including:
- Query analysis and optimization
- Index management and recommendations
- Multi-tenant query optimization
- Performance monitoring and metrics
- Caching strategies
"""
def __init__(self, tenant_schema: Optional[str] = None):
self.tenant_schema = tenant_schema
self.query_history = []
self.index_recommendations = []
self.optimization_stats = {
'queries_analyzed': 0,
'queries_optimized': 0,
'indexes_created': 0,
'performance_improvement': 0.0
}
@contextmanager
def monitor_query(self, query_text: str, optimization_level: OptimizationLevel = OptimizationLevel.BASIC):
"""
Context manager for monitoring query performance.
Args:
query_text: Description of the query being monitored
optimization_level: Level of optimization applied
"""
start_time = time.time()
rows_affected = 0
index_used = None
table_name = ""
try:
# Enable query logging
with connection.cursor() as cursor:
cursor.execute("SET log_statement = 'all'")
yield
# Get query metrics after execution
with connection.cursor() as cursor:
# Get the last executed query
cursor.execute("""
SELECT query, calls, total_time, rows,
pg_stat_statements.idx_scan as index_used
FROM pg_stat_statements
ORDER BY total_time DESC
LIMIT 1
""")
result = cursor.fetchone()
if result:
query_info, calls, total_time, rows, idx_scan = result
rows_affected = rows or 0
index_used = idx_scan
# Extract table name from query
if 'FROM' in query_info:
table_part = query_info.split('FROM')[1].split()[0]
table_name = table_part.strip('"')
# Calculate execution time
execution_time = time.time() - start_time
# Record metrics
metrics = QueryMetrics(
query_text=query_text,
execution_time=execution_time,
rows_affected=rows_affected,
index_used=index_used,
table_name=table_name,
timestamp=timezone.now(),
optimization_level=optimization_level,
tenant_schema=self.tenant_schema or 'public'
)
self.query_history.append(metrics)
self.optimization_stats['queries_analyzed'] += 1
# Log slow queries
if execution_time > 1.0: # More than 1 second
logger.warning(f"Slow query detected: {query_text} took {execution_time:.2f}s")
except Exception as e:
logger.error(f"Error monitoring query: {e}")
raise
finally:
# Reset query logging
with connection.cursor() as cursor:
cursor.execute("SET log_statement = 'mod'")
def optimize_tenant_queries(self, model_class: type, tenant_schema: str) -> Dict[str, Any]:
"""
Optimize queries for multi-tenant architecture.
Args:
model_class: Django model class to optimize
tenant_schema: Tenant schema name
Returns:
Dictionary with optimization results
"""
optimization_results = {
'tenant': tenant_schema,
'model': model_class.__name__,
'queries_optimized': 0,
'indexes_recommended': [],
'performance_improvements': []
}
with schema_context(tenant_schema):
# Analyze current query patterns
self._analyze_model_queries(model_class, optimization_results)
# Recommend indexes based on query patterns
recommendations = self._recommend_indexes(model_class, tenant_schema)
optimization_results['indexes_recommended'] = recommendations
# Optimize common query patterns
improvements = self._optimize_common_patterns(model_class, tenant_schema)
optimization_results['performance_improvements'] = improvements
return optimization_results
def _analyze_model_queries(self, model_class: type, results: Dict[str, Any]):
"""Analyze query patterns for a specific model."""
# Get all field names for the model
field_names = [field.name for field in model_class._meta.fields]
# Common query patterns to analyze
common_patterns = [
{'type': 'filter_by_id', 'fields': ['id']},
{'type': 'filter_by_tenant', 'fields': ['tenant']},
{'type': 'filter_by_status', 'fields': ['status']},
{'type': 'filter_by_date_range', 'fields': ['created_at', 'updated_at']},
{'type': 'filter_by_foreign_key', 'fields': [f for f in field_names if f.endswith('_id')]}
]
with connection.cursor() as cursor:
for pattern in common_patterns:
if any(field in field_names for field in pattern['fields']):
# Get query statistics for this pattern
query_stats = self._get_pattern_statistics(model_class, pattern, cursor)
results['queries_optimized'] += query_stats.get('total_queries', 0)
def _recommend_indexes(self, model_class: type, tenant_schema: str) -> List[IndexRecommendation]:
"""Generate index recommendations based on query patterns."""
recommendations = []
table_name = model_class._meta.db_table
# Get field information
fields = model_class._meta.fields
# Basic indexes for multi-tenant architecture
if hasattr(model_class, 'tenant'):
recommendations.append(IndexRecommendation(
table_name=table_name,
column_names=['tenant_id'],
index_type='btree',
expected_impact='High - Essential for multi-tenant isolation',
priority='Critical',
query_patterns=['All tenant-specific queries']
))
# Primary key index
pk_field = model_class._meta.pk
if pk_field and not pk_field.auto_created:
recommendations.append(IndexRecommendation(
table_name=table_name,
column_names=[pk_field.name],
index_type='btree',
expected_impact='High - Primary key lookups',
priority='High',
query_patterns=['Primary key queries']
))
# Foreign key indexes
for field in fields:
if field.is_relation and field.concrete:
recommendations.append(IndexRecommendation(
table_name=table_name,
column_names=[field.name],
index_type='btree',
expected_impact='Medium - Foreign key joins',
priority='Medium',
query_patterns=[f'Joins with {field.related_model.__name__}']
))
# Date/time indexes for temporal queries
date_fields = [f.name for f in fields if isinstance(f, (models.DateTimeField, models.DateField))]
if date_fields:
recommendations.append(IndexRecommendation(
table_name=table_name,
column_names=date_fields,
index_type='btree',
expected_impact='Medium - Date range queries',
priority='Medium',
query_patterns=['Date range queries', 'Time-based filtering']
))
# Status and enum fields
status_fields = [f.name for f in fields if f.name in ['status', 'state', 'is_active']]
if status_fields:
recommendations.append(IndexRecommendation(
table_name=table_name,
column_names=status_fields,
index_type='btree',
expected_impact='Medium - Status filtering',
priority='Medium',
query_patterns=['Status-based queries']
))
return recommendations
def _optimize_common_patterns(self, model_class: type, tenant_schema: str) -> List[str]:
"""Optimize common query patterns."""
improvements = []
# Optimize tenant-scoped queries
if hasattr(model_class, 'tenant'):
improvements.append(
"Added tenant_id to all queries for proper multi-tenant isolation"
)
# Optimize pagination queries
improvements.append(
"Implemented cursor-based pagination for large datasets"
)
# Optimize selective field queries
improvements.append(
"Added select_related/prefetch_related for efficient relationship loading"
)
return improvements
def create_recommended_indexes(self, recommendations: List[IndexRecommendation]) -> List[str]:
"""
Create recommended database indexes.
Args:
recommendations: List of index recommendations
Returns:
List of created index names
"""
created_indexes = []
with connection.cursor() as cursor:
for recommendation in recommendations:
if recommendation.priority == 'Critical':
index_name = f"idx_{recommendation.table_name}_{'_'.join(recommendation.column_names)}"
try:
# Create the index
column_list = ', '.join(recommendation.column_names)
create_sql = f"""
CREATE INDEX CONCURRENTLY IF NOT EXISTS {index_name}
ON {recommendation.table_name} ({column_list})
"""
cursor.execute(create_sql)
created_indexes.append(index_name)
logger.info(f"Created index: {index_name}")
self.optimization_stats['indexes_created'] += 1
except Exception as e:
logger.error(f"Failed to create index {index_name}: {e}")
return created_indexes
def analyze_query_performance(self, hours: int = 24) -> Dict[str, Any]:
"""
Analyze query performance over a specified time period.
Args:
hours: Number of hours to analyze
Returns:
Dictionary with performance analysis results
"""
analysis = {
'period_hours': hours,
'total_queries': 0,
'slow_queries': 0,
'avg_execution_time': 0.0,
'most_used_tables': [],
'performance_issues': [],
'recommendations': []
}
with connection.cursor() as cursor:
# Get query statistics
cursor.execute("""
SELECT
COUNT(*) as total_queries,
AVG(total_time) as avg_time,
COUNT(CASE WHEN total_time > 1000 THEN 1 END) as slow_queries
FROM pg_stat_statements
WHERE pg_stat_statements.query_start > NOW() - INTERVAL '%s hours'
""" % hours)
result = cursor.fetchone()
if result:
analysis['total_queries'] = result[0] or 0
analysis['avg_execution_time'] = result[1] or 0.0
analysis['slow_queries'] = result[2] or 0
# Get most used tables
cursor.execute("""
SELECT
schemaname,
relname,
seq_scan,
seq_tup_read,
idx_scan,
idx_tup_fetch
FROM pg_stat_user_tables
ORDER BY seq_scan + idx_scan DESC
LIMIT 10
""")
analysis['most_used_tables'] = [
{
'schema': row[0],
'table': row[1],
'sequential_scans': row[2],
'rows_read': row[3],
'index_scans': row[4],
'rows_fetched': row[5]
}
for row in cursor.fetchall()
]
# Identify performance issues
if analysis['slow_queries'] > 0:
analysis['performance_issues'].append(
f"Found {analysis['slow_queries']} slow queries (>1 second)"
)
if analysis['avg_execution_time'] > 0.5:
analysis['performance_issues'].append(
"Average query time is high (>0.5 seconds)"
)
return analysis
def optimize_malaysian_queries(self) -> Dict[str, Any]:
"""
Optimize queries specific to Malaysian market requirements.
Returns:
Dictionary with Malaysian-specific optimizations
"""
optimizations = {
'malaysian_optimizations': [],
'sst_queries_optimized': 0,
'ic_validation_optimized': False,
'address_queries_optimized': 0,
'localization_improvements': []
}
# Optimize SST calculation queries
optimizations['sst_queries_optimized'] = self._optimize_sst_queries()
# Optimize Malaysian IC validation queries
optimizations['ic_validation_optimized'] = self._optimize_ic_validation()
# Optimize Malaysian address queries
optimizations['address_queries_optimized'] = self._optimize_address_queries()
# Add localization improvements
optimizations['localization_improvements'] = [
"Added proper timezone handling for Malaysia (UTC+8)",
"Optimized multi-language field queries",
"Improved Malaysian state and postcode lookups",
"Enhanced business registration number queries"
]
return optimizations
def _optimize_sst_queries(self) -> int:
"""Optimize SST (Sales and Service Tax) calculation queries."""
optimized_count = 0
# Create indexes for SST-related fields
sst_indexes = [
"CREATE INDEX IF NOT EXISTS idx_sst_rate ON core_sstrate (rate)",
"CREATE INDEX IF NOT EXISTS idx_sst_category ON core_sstcategory (code)",
"CREATE INDEX IF NOT EXISTS idx_transaction_sst ON core_transaction (sst_amount, sst_rate)"
]
with connection.cursor() as cursor:
for index_sql in sst_indexes:
try:
cursor.execute(index_sql)
optimized_count += 1
except Exception as e:
logger.error(f"Failed to create SST index: {e}")
return optimized_count
def _optimize_ic_validation(self) -> bool:
"""Optimize Malaysian IC number validation queries."""
success = False
# Create index for Malaysian IC numbers
ic_indexes = [
"CREATE INDEX IF NOT EXISTS idx_user_ic_number ON core_user (ic_number)",
"CREATE INDEX IF NOT EXISTS idx_patient_ic ON healthcare_patient (ic_number)",
"CREATE INDEX IF NOT EXISTS idx_student_ic ON education_student (ic_number)"
]
with connection.cursor() as cursor:
try:
for index_sql in ic_indexes:
cursor.execute(index_sql)
success = True
except Exception as e:
logger.error(f"Failed to create IC validation indexes: {e}")
return success
def _optimize_address_queries(self) -> int:
"""Optimize Malaysian address-related queries."""
optimized_count = 0
# Create indexes for Malaysian addresses
address_indexes = [
"CREATE INDEX IF NOT EXISTS idx_address_postcode ON core_address (postcode)",
"CREATE INDEX IF NOT EXISTS idx_address_state ON core_address (state)",
"CREATE INDEX IF NOT EXISTS idx_address_city ON core_address (city)",
"CREATE INDEX IF NOT EXISTS idx_business_registration ON core_business (registration_number)"
]
with connection.cursor() as cursor:
for index_sql in address_indexes:
try:
cursor.execute(index_sql)
optimized_count += 1
except Exception as e:
logger.error(f"Failed to create address index: {e}")
return optimized_count
def get_optimization_report(self) -> Dict[str, Any]:
"""
Generate comprehensive optimization report.
Returns:
Dictionary with optimization report
"""
return {
'optimization_statistics': self.optimization_stats,
'query_history_summary': self._summarize_query_history(),
'current_recommendations': self.index_recommendations,
'malaysian_optimizations': self.optimize_malaysian_queries(),
'performance_analysis': self.analyze_query_performance(),
'suggested_actions': self._get_suggested_actions()
}
def _summarize_query_history(self) -> Dict[str, Any]:
"""Summarize query history metrics."""
if not self.query_history:
return {'total_queries': 0, 'average_time': 0.0}
total_queries = len(self.query_history)
total_time = sum(q.execution_time for q in self.query_history)
avg_time = total_time / total_queries if total_queries > 0 else 0.0
slow_queries = [q for q in self.query_history if q.execution_time > 1.0]
return {
'total_queries': total_queries,
'average_time': avg_time,
'slow_queries_count': len(slow_queries),
'slowest_query_time': max(q.execution_time for q in self.query_history),
'tables_queried': list(set(q.table_name for q in self.query_history))
}
def _get_suggested_actions(self) -> List[str]:
"""Get suggested optimization actions."""
actions = []
if self.optimization_stats['queries_analyzed'] > 0:
slow_percentage = (len([q for q in self.query_history if q.execution_time > 1.0]) /
len(self.query_history)) * 100
if slow_percentage > 10:
actions.append("High percentage of slow queries detected - consider query optimization")
if self.optimization_stats['indexes_created'] == 0:
actions.append("No indexes created - consider adding indexes for frequently queried fields")
actions.extend([
"Schedule regular database maintenance",
"Implement query caching for frequently accessed data",
"Consider database partitioning for large tables",
"Monitor and optimize connection pooling"
])
return actions
def clear_optimization_history(self):
"""Clear optimization history and reset statistics."""
self.query_history = []
self.index_recommendations = []
self.optimization_stats = {
'queries_analyzed': 0,
'queries_optimized': 0,
'indexes_created': 0,
'performance_improvement': 0.0
}
logger.info("Optimization history cleared")
class QueryOptimizer:
"""
Query-specific optimization utilities for common patterns.
"""
@staticmethod
def optimize_tenant_filter(queryset, tenant_id):
"""Optimize tenant-scoped queries."""
return queryset.filter(tenant_id=tenant_id).select_related('tenant')
@staticmethod
def optimize_pagination(queryset, page_size=50):
"""Optimize pagination for large datasets."""
return queryset.order_by('id')[:page_size]
@staticmethod
def optimize_foreign_key_query(queryset, related_fields):
"""Optimize queries with foreign key relationships."""
return queryset.select_related(*related_fields)
@staticmethod
def optimize_many_to_many_query(queryset, related_fields):
"""Optimize many-to-many relationship queries."""
return queryset.prefetch_related(*related_fields)
@staticmethod
def optimize_date_range_query(queryset, date_field, start_date, end_date):
"""Optimize date range queries."""
return queryset.filter(
**{f"{date_field}__gte": start_date,
f"{date_field}__lte": end_date}
).order_by(date_field)
@staticmethod
def optimize_full_text_search(queryset, search_fields, search_term):
"""Optimize full-text search queries."""
from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank
search_vector = SearchVector(*search_fields)
search_query = SearchQuery(search_term)
return queryset.annotate(
rank=SearchRank(search_vector, search_query)
).filter(rank__gte=0.3).order_by('-rank')
# Cache management utilities
class CacheManager:
"""Cache management for database optimization."""
@staticmethod
def get_cache_key(prefix: str, *args) -> str:
"""Generate cache key with prefix and arguments."""
return f"{prefix}_{'_'.join(str(arg) for arg in args)}"
@staticmethod
def cache_query_result(cache_key: str, query_result, timeout=3600):
"""Cache query result with specified timeout."""
cache.set(cache_key, query_result, timeout)
@staticmethod
def get_cached_result(cache_key: str):
"""Get cached result if available."""
return cache.get(cache_key)
@staticmethod
def invalidate_cache_pattern(pattern: str):
"""Invalidate cache keys matching pattern."""
keys = cache.keys(pattern)
if keys:
cache.delete_many(keys)
# Database maintenance utilities
class DatabaseMaintenance:
"""Database maintenance and optimization utilities."""
@staticmethod
def analyze_tables():
"""Run ANALYZE on all tables to update statistics."""
with connection.cursor() as cursor:
cursor.execute("""
SELECT schemaname || '.' || tablename
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
""")
tables = [row[0] for row in cursor.fetchall()]
for table in tables:
try:
cursor.execute(f"ANALYZE {table}")
logger.info(f"Analyzed table: {table}")
except Exception as e:
logger.error(f"Failed to analyze {table}: {e}")
@staticmethod
def vacuum_tables():
"""Run VACUUM on all tables to reclaim storage."""
with connection.cursor() as cursor:
cursor.execute("""
SELECT schemaname || '.' || tablename
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
""")
tables = [row[0] for row in cursor.fetchall()]
for table in tables:
try:
cursor.execute(f"VACUUM ANALYZE {table}")
logger.info(f"Vacuumed table: {table}")
except Exception as e:
logger.error(f"Failed to vacuum {table}: {e}")
@staticmethod
def get_table_sizes():
"""Get size information for all tables."""
with connection.cursor() as cursor:
cursor.execute("""
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
pg_total_relation_size(schemaname||'.'||tablename) as size_bytes
FROM pg_tables
WHERE schemaname NOT IN ('information_schema', 'pg_catalog')
ORDER BY size_bytes DESC
""")
return [
{
'schema': row[0],
'table': row[1],
'size': row[2],
'size_bytes': row[3]
}
for row in cursor.fetchall()
]
# Management command for database optimization
class OptimizationCommand:
"""Management command for database optimization."""
def handle(self, *args, **options):
"""Handle the optimization command."""
optimizer = DatabaseOptimizer()
# Analyze current performance
performance_analysis = optimizer.analyze_query_performance()
# Get optimization recommendations
report = optimizer.get_optimization_report()
# Create recommended indexes
if report['current_recommendations']:
created = optimizer.create_recommended_indexes(
report['current_recommendations']
)
print(f"Created {len(created)} new indexes")
# Optimize Malaysian-specific queries
malaysian_opts = optimizer.optimize_malaysian_queries()
print(f"Optimized {malaysian_opts['sst_queries_optimized']} SST queries")
# Run maintenance
DatabaseMaintenance.analyze_tables()
print("Database maintenance completed")
print("Optimization completed successfully")
print(f"Total queries analyzed: {optimizer.optimization_stats['queries_analyzed']}")
print(f"Indexes created: {optimizer.optimization_stats['indexes_created']}")
# Export main classes and functions
__all__ = [
'DatabaseOptimizer',
'QueryOptimizer',
'CacheManager',
'DatabaseMaintenance',
'OptimizationCommand',
'OptimizationLevel',
'QueryMetrics',
'IndexRecommendation',
]

269
backend/core/settings.py Normal file
View File

@@ -0,0 +1,269 @@
"""
Django settings for multi-tenant SaaS platform.
"""
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-key-for-development')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('DEBUG', 'True').lower() == 'true'
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'rest_framework',
'corsheaders',
'django_filters',
'django_extensions',
# Multi-tenant support
'django_tenants',
# Core apps
'core.tenants',
'core.users',
'core.auth',
# Module apps
'modules.retail',
'modules.healthcare',
'modules.education',
'modules.logistics',
'modules.beauty',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
# Multi-tenant middleware
'django_tenants.middleware.main.TenantMainMiddleware',
'core.middleware.tenant_middleware.TenantMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django_tenants.postgresql_backend',
'NAME': os.getenv('DB_NAME', 'saas_platform'),
'USER': os.getenv('DB_USER', 'postgres'),
'PASSWORD': os.getenv('DB_PASSWORD', 'devpass'),
'HOST': os.getenv('DB_HOST', 'localhost'),
'PORT': os.getenv('DB_PORT', '5432'),
}
}
# Multi-tenant configuration
TENANT_MODEL = 'tenants.Tenant'
TENANT_DOMAIN_MODEL = 'tenants.Domain'
TENANT_CACHE_PREFIX = 'tenant_'
# Password validation
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Kuala_Lumpur'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Django REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 20,
'DEFAULT_FILTER_BACKENDS': [
'django_filters.rest_framework.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
],
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour',
'user': '1000/hour',
},
}
# CORS settings
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000",
"http://127.0.0.1:3000",
]
CORS_ALLOW_CREDENTIALS = True
# JWT settings
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=60),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': os.getenv('JWT_SIGNING_KEY', SECRET_KEY),
}
# Redis settings
REDIS_URL = os.getenv('REDIS_URL', 'redis://localhost:6379/0')
# Celery settings
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = TIME_ZONE
# Stripe settings
STRIPE_SECRET_KEY = os.getenv('STRIPE_SECRET_KEY', '')
STRIPE_PUBLISHABLE_KEY = os.getenv('STRIPE_PUBLISHABLE_KEY', '')
STRIPE_WEBHOOK_SECRET = os.getenv('STRIPE_WEBHOOK_SECRET', '')
# Midtrans settings
MIDTRANS_SERVER_KEY = os.getenv('MIDTRANS_SERVER_KEY', '')
MIDTRANS_CLIENT_KEY = os.getenv('MIDTRANS_CLIENT_KEY', '')
# Email settings
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_HOST = os.getenv('EMAIL_HOST', '')
EMAIL_PORT = int(os.getenv('EMAIL_PORT', '587'))
EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS', 'True').lower() == 'true'
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER', '')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD', '')
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {process:d} {thread:d} {message}',
'style': '{',
},
'simple': {
'format': '{levelname} {message}',
'style': '{',
},
},
'handlers': {
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'verbose',
},
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': 'saas_platform.log',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
'core': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
'modules': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False,
},
},
}
# Security settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_HSTS_SECONDS = 31536000
# Healthcare compliance
HEALTHCARE_DATA_ENCRYPTION = os.getenv('HEALTHCARE_DATA_ENCRYPTION', 'True').lower() == 'true'
AUDIT_LOG_ENABLED = os.getenv('AUDIT_LOG_ENABLED', 'True').lower() == 'true'
# Data retention
DATA_RETENTION_DAYS = int(os.getenv('DATA_RETENTION_DAYS', '90'))

34
backend/core/urls.py Normal file
View File

@@ -0,0 +1,34 @@
"""
URL configuration for multi-tenant SaaS platform.
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# API endpoints
path('api/v1/auth/', include('core.auth.urls')),
path('api/v1/tenants/', include('core.tenants.urls')),
path('api/v1/users/', include('core.users.urls')),
path('api/v1/subscriptions/', include('core.subscriptions.urls')),
path('api/v1/modules/', include('core.modules.urls')),
path('api/v1/payments/', include('core.payments.urls')),
# Module endpoints
path('api/v1/retail/', include('modules.retail.urls')),
path('api/v1/healthcare/', include('modules.healthcare.urls')),
path('api/v1/education/', include('modules.education.urls')),
path('api/v1/logistics/', include('modules.logistics.urls')),
path('api/v1/beauty/', include('modules.beauty.urls')),
# API documentation
path('api/docs/', include('rest_framework.urls', namespace='rest_framework')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

11
backend/core/wsgi.py Normal file
View File

@@ -0,0 +1,11 @@
"""
WSGI config for multi-tenant SaaS platform.
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()