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
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:
0
backend/core/__init__.py
Normal file
0
backend/core/__init__.py
Normal file
11
backend/core/asgi.py
Normal file
11
backend/core/asgi.py
Normal 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()
|
||||
429
backend/core/caching/cache_manager.py
Normal file
429
backend/core/caching/cache_manager.py
Normal 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)
|
||||
403
backend/core/caching/django_integration.py
Normal file
403
backend/core/caching/django_integration.py
Normal 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)
|
||||
399
backend/core/caching/strategies.py
Normal file
399
backend/core/caching/strategies.py
Normal 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]
|
||||
616
backend/core/management/commands/cache_management.py
Normal file
616
backend/core/management/commands/cache_management.py
Normal 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
|
||||
554
backend/core/management/commands/optimize_database.py
Normal file
554
backend/core/management/commands/optimize_database.py
Normal 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()
|
||||
627
backend/core/optimization/config.py
Normal file
627
backend/core/optimization/config.py
Normal 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',
|
||||
]
|
||||
865
backend/core/optimization/index_manager.py
Normal file
865
backend/core/optimization/index_manager.py
Normal 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',
|
||||
]
|
||||
775
backend/core/optimization/query_optimization.py
Normal file
775
backend/core/optimization/query_optimization.py
Normal 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
269
backend/core/settings.py
Normal 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
34
backend/core/urls.py
Normal 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
11
backend/core/wsgi.py
Normal 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()
|
||||
Reference in New Issue
Block a user