mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 22:02:23 +08:00
482 lines
24 KiB
Python
482 lines
24 KiB
Python
import ssl
|
|
from collections.abc import Iterator
|
|
from datetime import timedelta
|
|
from importlib import import_module
|
|
from types import SimpleNamespace
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
from celery.contrib.testing.app import TestApp, setup_default_app
|
|
|
|
from dify_app import DifyApp
|
|
from enums import DeploymentEdition
|
|
from extensions.ext_celery import (
|
|
_enqueue_initial_community_telemetry_heartbeat,
|
|
get_celery_broker_transport_options,
|
|
init_app,
|
|
)
|
|
from services.knowledge_fs.background_contract import OPERATION_QUEUES, OPERATION_TASK
|
|
from services.knowledge_fs.lifecycle_readiness import KnowledgeFSLifecycleWorkerReadiness
|
|
from tests.helpers.unit.config_override import apply_config_overrides
|
|
|
|
"""Tests for Celery SSL configuration."""
|
|
|
|
|
|
class TestCelerySslCases:
|
|
@staticmethod
|
|
@pytest.fixture(autouse=True)
|
|
def restore_celery_app_state() -> Iterator[None]:
|
|
# init_app replaces Celery's process-wide current/default app. Restore both
|
|
# so later shared tasks do not enter this module's Flask app context.
|
|
with setup_default_app(TestApp()):
|
|
yield
|
|
|
|
class TestCelerySSLConfiguration:
|
|
"""Test suite for Celery SSL configuration."""
|
|
|
|
def test_get_celery_broker_transport_options_includes_global_keyprefix_for_redis(self):
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_USE_SENTINEL = False
|
|
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_broker_transport_options
|
|
|
|
result = get_celery_broker_transport_options()
|
|
|
|
assert result["global_keyprefix"] == "enterprise-a:"
|
|
|
|
def test_get_celery_broker_transport_options_omits_global_keyprefix_when_prefix_empty(self):
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_USE_SENTINEL = False
|
|
mock_config.REDIS_KEY_PREFIX = " "
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_broker_transport_options
|
|
|
|
result = get_celery_broker_transport_options()
|
|
|
|
assert "global_keyprefix" not in result
|
|
|
|
def test_get_celery_broker_transport_options_keeps_sentinel_and_adds_global_keyprefix(self):
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_USE_SENTINEL = True
|
|
mock_config.CELERY_SENTINEL_MASTER_NAME = "mymaster"
|
|
mock_config.CELERY_SENTINEL_SOCKET_TIMEOUT = 0.1
|
|
mock_config.CELERY_SENTINEL_PASSWORD = "secret"
|
|
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_broker_transport_options
|
|
|
|
result = get_celery_broker_transport_options()
|
|
|
|
assert result["master_name"] == "mymaster"
|
|
assert result["sentinel_kwargs"]["password"] == "secret"
|
|
assert result["global_keyprefix"] == "enterprise-a:"
|
|
|
|
def test_get_celery_ssl_options_when_ssl_disabled(self):
|
|
"""Test SSL options when BROKER_USE_SSL is False."""
|
|
from configs import DifyConfig
|
|
|
|
dify_config = DifyConfig(CELERY_BROKER_URL="redis://localhost:6379/0")
|
|
|
|
with patch("extensions.ext_celery.dify_config", dify_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is None
|
|
|
|
def test_get_celery_ssl_options_when_broker_not_redis(self):
|
|
"""Test SSL options when broker is not Redis."""
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_BROKER_URL = "amqp://localhost:5672"
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is None
|
|
|
|
def test_get_celery_ssl_options_with_cert_none(self):
|
|
"""Test SSL options with CERT_NONE requirement."""
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
mock_config.REDIS_SSL_CERT_REQS = "CERT_NONE"
|
|
mock_config.REDIS_SSL_CA_CERTS = None
|
|
mock_config.REDIS_SSL_CERTFILE = None
|
|
mock_config.REDIS_SSL_KEYFILE = None
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is not None
|
|
assert result["ssl_cert_reqs"] == ssl.CERT_NONE
|
|
assert result["ssl_ca_certs"] is None
|
|
assert result["ssl_certfile"] is None
|
|
assert result["ssl_keyfile"] is None
|
|
|
|
def test_get_celery_ssl_options_with_cert_required(self):
|
|
"""Test SSL options with CERT_REQUIRED and certificates."""
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_BROKER_URL = "rediss://localhost:6380/0"
|
|
mock_config.REDIS_SSL_CERT_REQS = "CERT_REQUIRED"
|
|
mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt"
|
|
mock_config.REDIS_SSL_CERTFILE = "/path/to/client.crt"
|
|
mock_config.REDIS_SSL_KEYFILE = "/path/to/client.key"
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is not None
|
|
assert result["ssl_cert_reqs"] == ssl.CERT_REQUIRED
|
|
assert result["ssl_ca_certs"] == "/path/to/ca.crt"
|
|
assert result["ssl_certfile"] == "/path/to/client.crt"
|
|
assert result["ssl_keyfile"] == "/path/to/client.key"
|
|
|
|
def test_get_celery_ssl_options_with_cert_optional(self):
|
|
"""Test SSL options with CERT_OPTIONAL requirement."""
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
mock_config.REDIS_SSL_CERT_REQS = "CERT_OPTIONAL"
|
|
mock_config.REDIS_SSL_CA_CERTS = "/path/to/ca.crt"
|
|
mock_config.REDIS_SSL_CERTFILE = None
|
|
mock_config.REDIS_SSL_KEYFILE = None
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is not None
|
|
assert result["ssl_cert_reqs"] == ssl.CERT_OPTIONAL
|
|
assert result["ssl_ca_certs"] == "/path/to/ca.crt"
|
|
|
|
def test_get_celery_ssl_options_with_invalid_cert_reqs(self):
|
|
"""Test SSL options with invalid cert requirement defaults to CERT_NONE."""
|
|
mock_config = MagicMock()
|
|
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
mock_config.REDIS_SSL_CERT_REQS = "INVALID_VALUE"
|
|
mock_config.REDIS_SSL_CA_CERTS = None
|
|
mock_config.REDIS_SSL_CERTFILE = None
|
|
mock_config.REDIS_SSL_KEYFILE = None
|
|
|
|
with patch("extensions.ext_celery.dify_config", mock_config):
|
|
from extensions.ext_celery import get_celery_ssl_options
|
|
|
|
result = get_celery_ssl_options()
|
|
assert result is not None
|
|
assert result["ssl_cert_reqs"] == ssl.CERT_NONE # Should default to CERT_NONE
|
|
|
|
def test_celery_init_applies_ssl_to_broker_and_backend(self):
|
|
"""Test that SSL options are applied to both broker and backend when using Redis."""
|
|
mock_config = MagicMock()
|
|
mock_config.HUMAN_INPUT_TIMEOUT_TASK_INTERVAL = 1
|
|
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
mock_config.CELERY_BACKEND = "redis"
|
|
mock_config.CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
|
|
mock_config.REDIS_SSL_CERT_REQS = "CERT_NONE"
|
|
mock_config.REDIS_SSL_CA_CERTS = None
|
|
mock_config.REDIS_SSL_CERTFILE = None
|
|
mock_config.REDIS_SSL_KEYFILE = None
|
|
mock_config.CELERY_USE_SENTINEL = False
|
|
mock_config.LOG_FORMAT = "%(message)s"
|
|
mock_config.LOG_TZ = "UTC"
|
|
mock_config.LOG_FILE = None
|
|
|
|
# Mock all the scheduler configs
|
|
mock_config.CELERY_BEAT_SCHEDULER_TIME = 1
|
|
mock_config.AGENT_SANDBOX_METERING_ENABLED = False
|
|
mock_config.ENABLE_CONVERSATION_CLEANUP_TASK = False
|
|
mock_config.TOKENER_NEW_TENANT_BOOTSTRAP_ENABLED = False
|
|
mock_config.ENABLE_TOKENER_BOOTSTRAP_RECOVERY_TASK = False
|
|
mock_config.ENABLE_TOKENER_MIGRATION_RECOVERY_TASK = False
|
|
mock_config.CONVERSATION_CLEANUP_TASK_INTERVAL = 5
|
|
mock_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK = False
|
|
mock_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK = False
|
|
mock_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK = False
|
|
mock_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK = False
|
|
mock_config.ENABLE_CLEAN_MESSAGES = False
|
|
mock_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK = False
|
|
mock_config.ENABLE_DATASETS_QUEUE_MONITOR = False
|
|
mock_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK = False
|
|
mock_config.ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK = False
|
|
mock_config.WORKFLOW_SCHEDULE_POLLER_INTERVAL = 1
|
|
mock_config.WORKFLOW_SCHEDULE_POLLER_BATCH_SIZE = 100
|
|
mock_config.WORKFLOW_SCHEDULE_MAX_DISPATCH_PER_TICK = 0
|
|
mock_config.ENABLE_TRIGGER_PROVIDER_REFRESH_TASK = False
|
|
mock_config.TRIGGER_PROVIDER_REFRESH_INTERVAL = 15
|
|
mock_config.ENABLE_API_TOKEN_LAST_USED_UPDATE_TASK = False
|
|
mock_config.API_TOKEN_LAST_USED_UPDATE_INTERVAL = 30
|
|
|
|
with (
|
|
patch("extensions.ext_celery.dify_config", mock_config),
|
|
patch(
|
|
"services.knowledge_fs.lifecycle_readiness.get_configured_knowledge_fs_lifecycle_worker_readiness",
|
|
return_value=KnowledgeFSLifecycleWorkerReadiness(ready=False, blockers=()),
|
|
),
|
|
):
|
|
from dify_app import DifyApp
|
|
from extensions.ext_celery import init_app
|
|
|
|
app = DifyApp(__name__)
|
|
celery_app = init_app(app)
|
|
|
|
# Check that SSL options were applied
|
|
assert "broker_use_ssl" in celery_app.conf
|
|
assert celery_app.conf["broker_use_ssl"] is not None
|
|
assert celery_app.conf["broker_use_ssl"]["ssl_cert_reqs"] == ssl.CERT_NONE
|
|
|
|
# Check that SSL is also applied to Redis backend
|
|
assert "redis_backend_use_ssl" in celery_app.conf
|
|
assert celery_app.conf["redis_backend_use_ssl"] is not None
|
|
|
|
def test_celery_init_registers_required_agent_and_conversation_tasks(self):
|
|
mock_config = MagicMock()
|
|
mock_config.BROKER_USE_SSL = False
|
|
mock_config.REDIS_KEY_PREFIX = "enterprise-a"
|
|
mock_config.HUMAN_INPUT_TIMEOUT_TASK_INTERVAL = 1
|
|
mock_config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
mock_config.CELERY_BACKEND = "redis"
|
|
mock_config.CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
|
|
mock_config.CELERY_USE_SENTINEL = False
|
|
mock_config.LOG_FORMAT = "%(message)s"
|
|
mock_config.LOG_TZ = "UTC"
|
|
mock_config.LOG_FILE = None
|
|
mock_config.CELERY_TASK_ANNOTATIONS = {}
|
|
|
|
mock_config.CELERY_BEAT_SCHEDULER_TIME = 1
|
|
mock_config.AGENT_SANDBOX_METERING_ENABLED = False
|
|
mock_config.ENABLE_CONVERSATION_CLEANUP_TASK = True
|
|
mock_config.TOKENER_NEW_TENANT_BOOTSTRAP_ENABLED = True
|
|
mock_config.ENABLE_TOKENER_BOOTSTRAP_RECOVERY_TASK = True
|
|
mock_config.TOKENER_BOOTSTRAP_RECOVERY_TASK_INTERVAL = 7
|
|
mock_config.CONVERSATION_CLEANUP_TASK_INTERVAL = 5
|
|
mock_config.ENABLE_CLEAN_EMBEDDING_CACHE_TASK = False
|
|
mock_config.ENABLE_CLEAN_UNUSED_DATASETS_TASK = False
|
|
mock_config.ENABLE_CREATE_TIDB_SERVERLESS_TASK = False
|
|
mock_config.ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK = False
|
|
mock_config.ENABLE_CLEAN_MESSAGES = False
|
|
mock_config.ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK = False
|
|
mock_config.ENABLE_DATASETS_QUEUE_MONITOR = False
|
|
mock_config.ENABLE_HUMAN_INPUT_TIMEOUT_TASK = False
|
|
mock_config.ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK = False
|
|
mock_config.MARKETPLACE_ENABLED = False
|
|
mock_config.WORKFLOW_LOG_CLEANUP_ENABLED = False
|
|
mock_config.ENABLE_WORKFLOW_RUN_CLEANUP_TASK = False
|
|
mock_config.ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK = False
|
|
mock_config.WORKFLOW_SCHEDULE_POLLER_INTERVAL = 1
|
|
mock_config.ENABLE_TRIGGER_PROVIDER_REFRESH_TASK = False
|
|
mock_config.TRIGGER_PROVIDER_REFRESH_INTERVAL = 15
|
|
mock_config.ENABLE_API_TOKEN_LAST_USED_UPDATE_TASK = False
|
|
mock_config.API_TOKEN_LAST_USED_UPDATE_INTERVAL = 30
|
|
mock_config.DEPLOYMENT_EDITION = DeploymentEdition.COMMUNITY
|
|
mock_config.ENTERPRISE_TELEMETRY_ENABLED = False
|
|
|
|
with (
|
|
patch("extensions.ext_celery.dify_config", mock_config),
|
|
patch(
|
|
"services.knowledge_fs.lifecycle_readiness.get_configured_knowledge_fs_lifecycle_worker_readiness",
|
|
return_value=KnowledgeFSLifecycleWorkerReadiness(ready=False, blockers=()),
|
|
),
|
|
):
|
|
from dify_app import DifyApp
|
|
from extensions.ext_celery import init_app
|
|
|
|
app = DifyApp(__name__)
|
|
celery_app = init_app(app)
|
|
|
|
assert celery_app.conf["broker_transport_options"]["global_keyprefix"] == "enterprise-a:"
|
|
assert celery_app.conf["result_backend_transport_options"]["global_keyprefix"] == "enterprise-a:"
|
|
assert "tasks.collect_agent_resources_task" in celery_app.conf["imports"]
|
|
assert "tasks.delete_conversation_task" in celery_app.conf["imports"]
|
|
assert "tasks.bootstrap_tokener_tenant_task" in celery_app.conf["imports"]
|
|
recovery = celery_app.conf["beat_schedule"]["tokener_bootstrap_recovery_sweeper"]
|
|
assert recovery["task"] == "tasks.bootstrap_tokener_tenant_task.sweep_pending_tokener_integrations_task"
|
|
assert recovery["schedule"].total_seconds() == 7 * 60
|
|
assert "tasks.workflow_run_archive_deletion_tasks" in celery_app.conf["imports"]
|
|
import_module("tasks.workflow_run_archive_deletion_tasks")
|
|
task_name = "tasks.workflow_run_archive_deletion_tasks.delete_workflow_run_archive_task"
|
|
archive_deletion = celery_app.tasks[task_name]
|
|
assert archive_deletion.queue == "workflow_archive"
|
|
assert archive_deletion.acks_late is True
|
|
assert archive_deletion.reject_on_worker_lost is True
|
|
assert archive_deletion.autoretry_for == (Exception,)
|
|
assert archive_deletion.retry_kwargs["max_retries"] == 5
|
|
assert celery_app.conf["beat_schedule"]["conversation_cleanup_sweeper"]["task"] == (
|
|
"tasks.delete_conversation_task.sweep_deleted_conversations"
|
|
)
|
|
|
|
|
|
class TestCommunityTelemetryCeleryCases:
|
|
@staticmethod
|
|
def test_beat_start_enqueues_community_telemetry_heartbeat() -> None:
|
|
task = Mock()
|
|
sender = SimpleNamespace(
|
|
app=SimpleNamespace(
|
|
conf=SimpleNamespace(beat_schedule={"community_telemetry_heartbeat": {}}),
|
|
tasks={"community_telemetry.send_heartbeat": task},
|
|
)
|
|
)
|
|
|
|
_enqueue_initial_community_telemetry_heartbeat(sender)
|
|
|
|
task.apply_async.assert_called_once_with()
|
|
|
|
@staticmethod
|
|
def test_beat_start_skips_community_telemetry_when_not_scheduled() -> None:
|
|
task = Mock()
|
|
sender = SimpleNamespace(
|
|
app=SimpleNamespace(
|
|
conf=SimpleNamespace(beat_schedule={}),
|
|
tasks={"community_telemetry.send_heartbeat": task},
|
|
)
|
|
)
|
|
|
|
_enqueue_initial_community_telemetry_heartbeat(sender)
|
|
|
|
task.apply_async.assert_not_called()
|
|
|
|
@staticmethod
|
|
def test_beat_start_skips_community_telemetry_when_task_is_unavailable() -> None:
|
|
sender = SimpleNamespace(
|
|
app=SimpleNamespace(
|
|
conf=SimpleNamespace(beat_schedule={"community_telemetry_heartbeat": {}}),
|
|
tasks={},
|
|
)
|
|
)
|
|
|
|
_enqueue_initial_community_telemetry_heartbeat(sender)
|
|
|
|
|
|
class TestKnowledgeFsCeleryCases:
|
|
@staticmethod
|
|
@pytest.fixture(autouse=True)
|
|
def restore_celery_app_state() -> Iterator[None]:
|
|
# init_app replaces Celery's process-wide current/default app; restore both so
|
|
# later shared tasks still resolve against the app that registered them.
|
|
with setup_default_app(TestApp()):
|
|
yield
|
|
|
|
@staticmethod
|
|
@pytest.mark.parametrize("background_enabled", [False, True])
|
|
@pytest.mark.parametrize("vector_store", ["qdrant", "tidb_on_qdrant", "elasticsearch", "weaviate"])
|
|
def test_celery_registers_initial_source_task_when_knowledge_fs_lifecycle_is_ready(
|
|
background_enabled: bool, vector_store: str, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# A stale deployment variable must not suppress vector cleanup for the selected provider.
|
|
monkeypatch.setenv("KNOWLEDGE_VECTOR_STORAGE", "postgres")
|
|
apply_config_overrides(
|
|
monkeypatch,
|
|
REDIS_KEY_PREFIX="test",
|
|
HUMAN_INPUT_TIMEOUT_TASK_INTERVAL=1,
|
|
CELERY_BROKER_URL="redis://localhost:6379/0",
|
|
CELERY_BACKEND="redis",
|
|
CELERY_USE_SENTINEL=False,
|
|
CELERY_BROKER_VISIBILITY_TIMEOUT=10800,
|
|
KNOWLEDGE_FS_BACKGROUND_WORKER_ENABLED=background_enabled,
|
|
VECTOR_STORE=vector_store,
|
|
KNOWLEDGE_FS_BACKGROUND_POLL_INTERVAL_SECONDS=5,
|
|
KNOWLEDGE_FS_BACKGROUND_TASK_TIMEOUT_SECONDS=7200,
|
|
LOG_FORMAT="%(message)s",
|
|
LOG_TZ="UTC",
|
|
LOG_FILE=None,
|
|
CELERY_TASK_ANNOTATIONS=dict[str, object](),
|
|
CELERY_BEAT_SCHEDULER_TIME=1,
|
|
KNOWLEDGE_FS_LIFECYCLE_POLL_INTERVAL_SECONDS=2,
|
|
ENABLE_CONVERSATION_CLEANUP_TASK=True,
|
|
CONVERSATION_CLEANUP_TASK_INTERVAL=5,
|
|
ENABLE_CLEAN_EMBEDDING_CACHE_TASK=False,
|
|
ENABLE_CLEAN_UNUSED_DATASETS_TASK=False,
|
|
ENABLE_CREATE_TIDB_SERVERLESS_TASK=False,
|
|
ENABLE_UPDATE_TIDB_SERVERLESS_STATUS_TASK=False,
|
|
ENABLE_CLEAN_MESSAGES=False,
|
|
ENABLE_MAIL_CLEAN_DOCUMENT_NOTIFY_TASK=False,
|
|
ENABLE_DATASETS_QUEUE_MONITOR=False,
|
|
ENABLE_HUMAN_INPUT_TIMEOUT_TASK=False,
|
|
ENABLE_CHECK_UPGRADABLE_PLUGIN_TASK=False,
|
|
MARKETPLACE_ENABLED=False,
|
|
WORKFLOW_LOG_CLEANUP_ENABLED=False,
|
|
ENABLE_WORKFLOW_RUN_CLEANUP_TASK=False,
|
|
ENABLE_WORKFLOW_SCHEDULE_POLLER_TASK=False,
|
|
WORKFLOW_SCHEDULE_POLLER_INTERVAL=1,
|
|
ENABLE_TRIGGER_PROVIDER_REFRESH_TASK=False,
|
|
TRIGGER_PROVIDER_REFRESH_INTERVAL=15,
|
|
ENABLE_API_TOKEN_LAST_USED_UPDATE_TASK=False,
|
|
API_TOKEN_LAST_USED_UPDATE_INTERVAL=30,
|
|
ENTERPRISE_TELEMETRY_ENABLED=False,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"services.knowledge_fs.lifecycle_readiness.get_configured_knowledge_fs_lifecycle_worker_readiness",
|
|
return_value=KnowledgeFSLifecycleWorkerReadiness(ready=True, blockers=()),
|
|
),
|
|
):
|
|
celery_app = init_app(DifyApp(__name__))
|
|
|
|
assert "tasks.knowledge_fs_initial_source_tasks" in celery_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_initial_source_preview_tasks" in celery_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_failed_retrieval_tasks" in celery_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_lifecycle_tasks" in celery_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_source_import_tasks" in celery_app.conf["imports"]
|
|
assert "tasks.delete_conversation_task" in celery_app.conf["imports"]
|
|
assert celery_app.conf["beat_schedule"]["conversation_cleanup_sweeper"] == {
|
|
"task": "tasks.delete_conversation_task.sweep_deleted_conversations",
|
|
"schedule": timedelta(minutes=5),
|
|
}
|
|
assert celery_app.conf["beat_schedule"]["knowledge_fs_staged_upload_cleanup"] == {
|
|
"task": "tasks.knowledge_fs_lifecycle_tasks.cleanup_knowledge_fs_staged_uploads",
|
|
"schedule": timedelta(seconds=2),
|
|
}
|
|
assert celery_app.conf["beat_schedule"]["knowledge_fs_upgrade_file_cleanup"] == {
|
|
"task": "tasks.knowledge_fs_upgrade_tasks.cleanup_deferred_knowledge_fs_upgrade_files",
|
|
"schedule": timedelta(seconds=2),
|
|
}
|
|
assert celery_app.conf["beat_schedule"]["knowledge_fs_upgrade_recovery"] == {
|
|
"task": "tasks.knowledge_fs_upgrade_tasks.recover_stalled_knowledge_fs_upgrades",
|
|
"schedule": timedelta(seconds=60),
|
|
"options": {"expires": 60},
|
|
}
|
|
schedules = celery_app.conf["beat_schedule"]
|
|
if background_enabled:
|
|
assert celery_app.conf["broker_transport_options"]["visibility_timeout"] == 10800
|
|
for operation, queue in OPERATION_QUEUES.items():
|
|
assert schedules[f"knowledge_fs_background_{operation}"] == {
|
|
"task": OPERATION_TASK,
|
|
"schedule": timedelta(seconds=5),
|
|
"kwargs": {"operation": operation},
|
|
"options": {"queue": queue, "expires": 30},
|
|
}
|
|
else:
|
|
assert not any(name.startswith("knowledge_fs_background_") for name in schedules)
|
|
|
|
with (
|
|
patch(
|
|
"services.knowledge_fs.lifecycle_readiness.get_configured_knowledge_fs_lifecycle_worker_readiness",
|
|
return_value=KnowledgeFSLifecycleWorkerReadiness(ready=False, blockers=("test",)),
|
|
),
|
|
):
|
|
preview_only_app = init_app(DifyApp(f"{__name__}.preview_only"))
|
|
|
|
assert "tasks.knowledge_fs_initial_source_preview_tasks" in preview_only_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_failed_retrieval_tasks" in preview_only_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_initial_source_tasks" not in preview_only_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_lifecycle_tasks" not in preview_only_app.conf["imports"]
|
|
assert "tasks.knowledge_fs_source_import_tasks" not in preview_only_app.conf["imports"]
|
|
assert "knowledge_fs_upgrade_recovery" not in preview_only_app.conf["beat_schedule"]
|
|
|
|
@staticmethod
|
|
@pytest.mark.parametrize("visibility", [None, 3600, 7200, 7230])
|
|
def test_long_running_delivery_rejects_an_unsafe_redis_visibility(visibility: int | None) -> None:
|
|
with patch("extensions.ext_celery.dify_config") as config:
|
|
config.CELERY_USE_SENTINEL = False
|
|
config.REDIS_KEY_PREFIX = ""
|
|
config.CELERY_BROKER_URL = "redis://localhost:6379/0"
|
|
config.CELERY_BROKER_VISIBILITY_TIMEOUT = visibility
|
|
config.KNOWLEDGE_FS_BACKGROUND_WORKER_ENABLED = True
|
|
config.KNOWLEDGE_FS_BACKGROUND_TASK_TIMEOUT_SECONDS = 7200
|
|
with pytest.raises(ValueError, match="VISIBILITY_TIMEOUT"):
|
|
get_celery_broker_transport_options()
|