diff --git a/api/configs/enterprise/__init__.py b/api/configs/enterprise/__init__.py index 11a71e1537..c7289de393 100644 --- a/api/configs/enterprise/__init__.py +++ b/api/configs/enterprise/__init__.py @@ -19,6 +19,10 @@ class EnterpriseFeatureConfig(BaseSettings): default=False, ) + ENTERPRISE_REQUEST_TIMEOUT: int = Field( + ge=1, description="Maximum timeout in seconds for enterprise requests", default=5 + ) + class EnterpriseTelemetryConfig(BaseSettings): """ diff --git a/api/services/enterprise/plugin_manager_service.py b/api/services/enterprise/plugin_manager_service.py index 48023f408c..d4be36305e 100644 --- a/api/services/enterprise/plugin_manager_service.py +++ b/api/services/enterprise/plugin_manager_service.py @@ -3,6 +3,7 @@ import logging from pydantic import BaseModel +from configs import dify_config from services.enterprise.base import EnterprisePluginManagerRequest from services.errors.base import BaseServiceError @@ -69,6 +70,7 @@ class PluginManagerService: "POST", "/pre-uninstall-plugin", json=body.model_dump(), + timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT, ) except Exception: logger.exception( diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index 411c335c17..d063c57b32 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -30,6 +30,7 @@ from extensions.ext_database import db from extensions.ext_redis import redis_client from models.provider import ProviderCredential from models.provider_ids import GenericProviderID +from services.enterprise.plugin_manager_service import PluginManagerService, PreUninstallPluginRequest from services.errors.plugin import PluginInstallationForbiddenError from services.feature_service import FeatureService, PluginInstallationScope @@ -518,6 +519,13 @@ class PluginService: if plugin: plugin_id = plugin.plugin_id logger.info("Deleting credentials for plugin: %s", plugin_id) + if dify_config.ENTERPRISE_ENABLED: + PluginManagerService.try_pre_uninstall_plugin( + PreUninstallPluginRequest( + tenant_id=tenant_id, + plugin_unique_identifier=plugin.plugin_unique_identifier, + ) + ) # Delete provider credentials that match this plugin credentials = db.session.scalars( diff --git a/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py b/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py index dc084b6eb8..bd81f0ff89 100644 --- a/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py +++ b/api/tests/unit_tests/services/enterprise/test_plugin_manager_service.py @@ -14,73 +14,82 @@ from services.enterprise.plugin_manager_service import ( PreUninstallPluginRequest, ) +_FAKE_TIMEOUT = 30 + + +_SEND_REQUEST_PATH = ( + "services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request" +) +_DIFY_CONFIG_PATH = "services.enterprise.plugin_manager_service.dify_config" +_LOGGER_PATH = "services.enterprise.plugin_manager_service.logger" + class TestTryPreUninstallPlugin: - def test_try_pre_uninstall_plugin_success(self): + @patch(_DIFY_CONFIG_PATH) + @patch(_SEND_REQUEST_PATH) + def test_try_pre_uninstall_plugin_success(self, mock_send_request, mock_config): body = PreUninstallPluginRequest( tenant_id="tenant-123", plugin_unique_identifier="com.example.my_plugin", ) + mock_config.ENTERPRISE_REQUEST_TIMEOUT = _FAKE_TIMEOUT + mock_send_request.return_value = {} - with patch( - "services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request" - ) as mock_send_request: - mock_send_request.return_value = {} + PluginManagerService.try_pre_uninstall_plugin(body) - PluginManagerService.try_pre_uninstall_plugin(body) + mock_send_request.assert_called_once_with( + "POST", + "/pre-uninstall-plugin", + json={"tenant_id": "tenant-123", "plugin_unique_identifier": "com.example.my_plugin"}, + timeout=_FAKE_TIMEOUT, + ) - mock_send_request.assert_called_once_with( - "POST", - "/pre-uninstall-plugin", - json={"tenant_id": "tenant-123", "plugin_unique_identifier": "com.example.my_plugin"}, - ) - - def test_try_pre_uninstall_plugin_http_error_soft_fails(self): + @patch(_DIFY_CONFIG_PATH) + @patch(_LOGGER_PATH) + @patch(_SEND_REQUEST_PATH) + def test_try_pre_uninstall_plugin_http_error_soft_fails( + self, mock_send_request, mock_logger, mock_config + ): body = PreUninstallPluginRequest( tenant_id="tenant-456", plugin_unique_identifier="com.example.other_plugin", ) + mock_config.ENTERPRISE_REQUEST_TIMEOUT = _FAKE_TIMEOUT + mock_send_request.side_effect = HTTPStatusError( + "502 Bad Gateway", + request=None, + response=None, + ) - with ( - patch( - "services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request" - ) as mock_send_request, - patch("services.enterprise.plugin_manager_service.logger") as mock_logger, - ): - mock_send_request.side_effect = HTTPStatusError( - "502 Bad Gateway", - request=None, - response=None, - ) + PluginManagerService.try_pre_uninstall_plugin(body) - PluginManagerService.try_pre_uninstall_plugin(body) + mock_send_request.assert_called_once_with( + "POST", + "/pre-uninstall-plugin", + json={"tenant_id": "tenant-456", "plugin_unique_identifier": "com.example.other_plugin"}, + timeout=_FAKE_TIMEOUT, + ) + mock_logger.exception.assert_called_once() - mock_send_request.assert_called_once_with( - "POST", - "/pre-uninstall-plugin", - json={"tenant_id": "tenant-456", "plugin_unique_identifier": "com.example.other_plugin"}, - ) - mock_logger.exception.assert_called_once() - - def test_try_pre_uninstall_plugin_generic_exception_soft_fails(self): + @patch(_DIFY_CONFIG_PATH) + @patch(_LOGGER_PATH) + @patch(_SEND_REQUEST_PATH) + def test_try_pre_uninstall_plugin_generic_exception_soft_fails( + self, mock_send_request, mock_logger, mock_config + ): body = PreUninstallPluginRequest( tenant_id="tenant-789", plugin_unique_identifier="com.example.failing_plugin", ) + mock_config.ENTERPRISE_REQUEST_TIMEOUT = _FAKE_TIMEOUT + mock_send_request.side_effect = ConnectionError("network unreachable") - with ( - patch( - "services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request" - ) as mock_send_request, - patch("services.enterprise.plugin_manager_service.logger") as mock_logger, - ): - mock_send_request.side_effect = ConnectionError("network unreachable") + PluginManagerService.try_pre_uninstall_plugin(body) - PluginManagerService.try_pre_uninstall_plugin(body) - - mock_send_request.assert_called_once_with( - "POST", - "/pre-uninstall-plugin", - json={"tenant_id": "tenant-789", "plugin_unique_identifier": "com.example.failing_plugin"}, - ) - mock_logger.exception.assert_called_once() + mock_send_request.assert_called_once_with( + "POST", + "/pre-uninstall-plugin", + json={"tenant_id": "tenant-789", "plugin_unique_identifier": "com.example.failing_plugin"}, + timeout=_FAKE_TIMEOUT, + ) + mock_logger.exception.assert_called_once()