From 92325bd12642d7bf79b953b574102e7f950ac40d Mon Sep 17 00:00:00 2001 From: Cordis798 <2378285593@qq.com> Date: Mon, 2 Nov 2026 10:35:15 +0800 Subject: [PATCH] fix(community): load retrieval TLS trust off the event loop (#7738) Co-authored-by: Cordis798 <284918518+Cordis798@users.noreply.github.com> --- README.md | 4 +- backend/AGENTS.md | 2 + .../deerflow/community/lightrag/client.py | 3 +- .../deerflow/community/ragflow/client.py | 3 +- .../blocking_io/test_retrieval_client_ssl.py | 52 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 backend/tests/blocking_io/test_retrieval_client_ssl.py diff --git a/README.md b/README.md index 8f88d25e43..5d5a5c4920 100644 --- a/README.md +++ b/README.md @@ -1703,7 +1703,9 @@ with bge-m3 and GLM-5.3; see that record for coverage limits. Configure `knowledge_search` with either `deerflow.community.ragflow.tools:knowledge_search_tool` or `deerflow.community.lightrag.tools:knowledge_search_tool` in `config.yaml`. -Both support attributable retrieval evidence. LightRAG and RAGFlow responses +Both support attributable retrieval evidence. Both clients load HTTP client +TLS trust in a worker thread before asynchronous requests, keeping synchronous +certificate loading off the event loop. LightRAG and RAGFlow responses without complete provider locators use the shared DeerFlow source contract; complete RAGFlow responses retain their existing citation snapshots and scope controls. Knowledge citations show the diff --git a/backend/AGENTS.md b/backend/AGENTS.md index aad9490f2b..dd954dba40 100644 --- a/backend/AGENTS.md +++ b/backend/AGENTS.md @@ -139,6 +139,8 @@ Read the nearest scoped `AGENTS.md` for subsystem ownership. Read-only knowledge: [contract](docs/KNOWLEDGE.md) covers independent retrieval tools, scope intersection and extension-owned providers. +RAGFlow/LightRAG clients offload HTTPX construction (synchronous TLS trust loading) +with `asyncio.to_thread`; keep request execution and session cleanup on the loop. ### Harness / App Split diff --git a/backend/packages/harness/deerflow/community/lightrag/client.py b/backend/packages/harness/deerflow/community/lightrag/client.py index 396252c10e..380d5176e6 100644 --- a/backend/packages/harness/deerflow/community/lightrag/client.py +++ b/backend/packages/harness/deerflow/community/lightrag/client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from typing import Any import httpx @@ -99,7 +100,7 @@ class LightRAGClient: client_kwargs["transport"] = self._transport try: - async with httpx.AsyncClient(**client_kwargs) as client: + async with await asyncio.to_thread(httpx.AsyncClient, **client_kwargs) as client: response = await client.request(method, path, json=json) except httpx.TimeoutException: raise LightRAGConnectionError(f"LightRAG request timed out after {self.timeout:g} seconds.") from None diff --git a/backend/packages/harness/deerflow/community/ragflow/client.py b/backend/packages/harness/deerflow/community/ragflow/client.py index a4782fc8c7..0256d4979e 100644 --- a/backend/packages/harness/deerflow/community/ragflow/client.py +++ b/backend/packages/harness/deerflow/community/ragflow/client.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import re from typing import Any @@ -78,7 +79,7 @@ class RAGFlowClient: client_kwargs["transport"] = self._transport try: - async with httpx.AsyncClient(**client_kwargs) as client: + async with await asyncio.to_thread(httpx.AsyncClient, **client_kwargs) as client: response = await client.request(method, path, params=params, json=json) except httpx.TimeoutException: raise RAGFlowConnectionError(f"RAGFlow request timed out after {self.timeout:g} seconds.") from None diff --git a/backend/tests/blocking_io/test_retrieval_client_ssl.py b/backend/tests/blocking_io/test_retrieval_client_ssl.py new file mode 100644 index 0000000000..1b1659c164 --- /dev/null +++ b/backend/tests/blocking_io/test_retrieval_client_ssl.py @@ -0,0 +1,52 @@ +"""Keep real HTTPX TLS setup; replace only the network response.""" + +import asyncio +import ssl + +import httpx +import pytest + +from deerflow.community.lightrag.client import LightRAGClient +from deerflow.community.ragflow.client import RAGFlowClient + + +@pytest.mark.asyncio +@pytest.mark.parametrize("provider", ["ragflow", "lightrag"]) +async def test_retrieval_client_loads_tls_trust_off_loop(provider, monkeypatch): + original = ssl.create_default_context + contexts = [] + clients = [] + + def ssl_context(*args, **kwargs): + try: + asyncio.get_running_loop() + except RuntimeError: + context = original(*args, **kwargs) + contexts.append(context) + return context + raise AssertionError("TLS trust loading must run off the event loop") + + async def send(self, request, **kwargs): + clients.append(self) + if provider == "ragflow": + assert request.url.path == "/api/v1/datasets" + assert request.url.params["ids"] == "synthetic-dataset" + assert request.headers["Authorization"] == "Bearer synthetic-key" + payload = {"code": 0, "data": [{"id": "synthetic-dataset"}]} + else: + assert request.url.path == "/query/data" + assert request.headers["X-API-Key"] == "synthetic-key" + payload = {"status": "success", "data": {"chunks": []}} + return httpx.Response(200, request=request, json=payload) + + monkeypatch.setattr(ssl, "create_default_context", ssl_context) + monkeypatch.setattr(httpx.AsyncClient, "send", send) + if provider == "ragflow": + client = RAGFlowClient(base_url="https://ragflow.invalid", api_key="synthetic-key") + assert await client.list_datasets(dataset_id="synthetic-dataset") == [{"id": "synthetic-dataset"}] + else: + client = LightRAGClient(base_url="https://lightrag.invalid", api_key="synthetic-key") + assert await client.query_data("synthetic query") == {"chunks": []} + assert contexts + assert len(clients) == 1 + assert clients[0].is_closed