diff --git a/api/app_factory.py b/api/app_factory.py index 8d9efe60ca..ebd5a78039 100644 --- a/api/app_factory.py +++ b/api/app_factory.py @@ -10,7 +10,7 @@ from contexts.wrapper import RecyclableContextVar from controllers.console.error import UnauthorizedAndForceLogout from core.logging.context import init_request_context from dify_app import DifyApp -from services.feature_service import FeatureService, LicenseStatus +from services.enterprise.enterprise_service import EnterpriseService logger = logging.getLogger(__name__) @@ -50,18 +50,13 @@ def create_flask_app_with_configs() -> DifyApp: if not is_exempt: try: - # Check license status - system_features = FeatureService.get_system_features(is_authenticated=True) - if system_features.license.status in [ - LicenseStatus.INACTIVE, - LicenseStatus.EXPIRED, - LicenseStatus.LOST, - ]: + # Check license status with caching (10 min TTL) + license_status = EnterpriseService.get_cached_license_status() + if license_status in ["inactive", "expired", "lost"]: # Raise UnauthorizedAndForceLogout to trigger frontend reload and logout # Frontend checks code === 'unauthorized_and_force_logout' and calls location.reload() raise UnauthorizedAndForceLogout( - f"Enterprise license is {system_features.license.status.value}. " - "Please contact your administrator." + f"Enterprise license is {license_status}. Please contact your administrator." ) except UnauthorizedAndForceLogout: # Re-raise to let Flask error handler convert to proper JSON response diff --git a/api/services/enterprise/enterprise_service.py b/api/services/enterprise/enterprise_service.py index 7a6ebf7c3f..c2d89283a6 100644 --- a/api/services/enterprise/enterprise_service.py +++ b/api/services/enterprise/enterprise_service.py @@ -5,12 +5,16 @@ from datetime import datetime from pydantic import BaseModel, ConfigDict, Field, model_validator from configs import dify_config +from extensions.ext_redis import redis_client from services.enterprise.base import EnterpriseRequest logger = logging.getLogger(__name__) DEFAULT_WORKSPACE_JOIN_TIMEOUT_SECONDS = 1.0 ALLOWED_ACCESS_MODES = ["public", "private", "private_all", "sso_verified"] +# License status cache configuration +LICENSE_STATUS_CACHE_KEY = "enterprise:license:status" +LICENSE_STATUS_CACHE_TTL = 600 # 10 minutes class WebAppSettings(BaseModel): @@ -224,3 +228,45 @@ class EnterpriseService: params = {"appId": app_id} EnterpriseRequest.send_request("DELETE", "/webapp/clean", params=params) + + @classmethod + def get_cached_license_status(cls): + """ + Get enterprise license status with Redis caching to reduce HTTP calls. + + Only caches valid statuses (active/expiring) since invalid statuses + should be re-checked every request — the admin may update the license + at any time. + + Returns license status string or None if unavailable. + """ + if not dify_config.ENTERPRISE_ENABLED: + return None + + # Try cache first — only valid statuses are cached + try: + cached_status = redis_client.get(LICENSE_STATUS_CACHE_KEY) + if cached_status: + if isinstance(cached_status, bytes): + cached_status = cached_status.decode("utf-8") + return cached_status + except Exception: + logger.debug("Failed to get license status from cache, calling enterprise API") + + # Cache miss or failure — call enterprise API + try: + info = cls.get_info() + license_info = info.get("License") + if license_info: + status = license_info.get("status", "inactive") + # Only cache valid statuses so license updates are picked up immediately + if status in ("active", "expiring"): + try: + redis_client.setex(LICENSE_STATUS_CACHE_KEY, LICENSE_STATUS_CACHE_TTL, status) + except Exception: + logger.debug("Failed to cache license status") + return status + except Exception: + logger.exception("Failed to get enterprise license status") + + return None \ No newline at end of file