refactor: 统一 Redis 连接管理,改用 RedisService

- App 启动时初始化 RedisService,关闭时释放连接
- Celery worker 通过 worker_process_init 钩子初始化 Redis
- Agent 端点改用 RedisService 替代直接创建连接
- Celery task 改为 async def,使用统一连接
- 删除无用的 infra 模块和 core/http/models
- 日志脱敏,不记录 Redis 密码
- 初始化失败时 fail-fast
- 异常发布添加二级保护
This commit is contained in:
qzl
2026-03-06 16:09:15 +08:00
parent c5ccfc4b88
commit 2c59fe5ee2
14 changed files with 101 additions and 150 deletions
@@ -31,6 +31,14 @@ class RedisStreamEventStore:
payload = json.dumps(event, ensure_ascii=True, separators=(",", ":"))
return str(self._client.xadd(stream, {"event": payload}))
async def append_event(self, *, session_id: UUID, event: dict[str, Any]) -> str:
stream = self._stream_name(session_id)
payload = json.dumps(event, ensure_ascii=True, separators=(",", ":"))
result = self._client.xadd(stream, {"event": payload})
if inspect.isawaitable(result):
return str(await result)
return str(result)
async def read_events(
self,
*,
@@ -1,28 +1,21 @@
from __future__ import annotations
import asyncio
import threading
from typing import Any, Callable, Protocol, cast
from typing import Any, Protocol, cast
from uuid import UUID
import redis
from core.agent.application.resume_service import ResumeService
from core.agent.application.run_service import RunService
from core.agent.infrastructure.events.redis_stream import RedisStreamEventStore
from core.celery.app import celery_app
from core.config.settings import config
from core.logging import get_logger
from services.base.redis import redis_service
logger = get_logger("core.agent.infrastructure.queue.tasks")
_background_loop: asyncio.AbstractEventLoop | None = None
_background_thread: threading.Thread | None = None
_background_ready = threading.Event()
class PublishEvent(Protocol):
def __call__(self, event_type: str, payload: dict[str, object]) -> None: ...
async def __call__(self, event_type: str, payload: dict[str, object]) -> None: ...
class RunServiceLike(Protocol):
@@ -35,36 +28,9 @@ class ResumeServiceLike(Protocol):
) -> dict[str, object]: ...
def _run_async(task: Callable[[], Any]) -> Any:
loop = _ensure_background_loop()
future = asyncio.run_coroutine_threadsafe(task(), loop)
return future.result()
def _ensure_background_loop() -> asyncio.AbstractEventLoop:
global _background_loop, _background_thread
if _background_loop is not None:
return _background_loop
def _loop_worker() -> None:
global _background_loop
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
_background_loop = loop
_background_ready.set()
loop.run_forever()
_background_thread = threading.Thread(target=_loop_worker, daemon=True)
_background_thread.start()
_background_ready.wait(timeout=5)
if _background_loop is None:
raise RuntimeError("failed to initialize background event loop")
return _background_loop
def _build_redis_publisher() -> PublishEvent:
async def _build_redis_publisher() -> PublishEvent:
settings = cast(Any, config)
client = redis.from_url(settings.redis.url, decode_responses=True)
client = redis_service.get_client()
event_store = RedisStreamEventStore(
client=client,
stream_prefix=settings.agent_runtime.redis_stream_prefix,
@@ -72,11 +38,11 @@ def _build_redis_publisher() -> PublishEvent:
block_ms=settings.agent_runtime.redis_stream_block_ms,
)
def _publish(event_type: str, payload: dict[str, object]) -> None:
async def _publish(event_type: str, payload: dict[str, object]) -> None:
session_id = str(payload.get("session_id", "")).strip()
if not session_id:
raise ValueError("session_id is required in event payload")
event_store.append_event_sync(
await event_store.append_event(
session_id=UUID(session_id),
event={"type": event_type, "data": payload},
)
@@ -84,14 +50,14 @@ def _build_redis_publisher() -> PublishEvent:
return _publish
def run_agent_task(
async def run_agent_task(
command: dict[str, Any],
*,
publish_event: PublishEvent | None = None,
run_service: RunServiceLike | None = None,
resume_service: ResumeServiceLike | None = None,
) -> dict[str, object]:
publisher = publish_event or _build_redis_publisher()
publisher = publish_event or await _build_redis_publisher()
service_run = run_service or RunService()
service_resume = resume_service or ResumeService()
@@ -105,31 +71,27 @@ def run_agent_task(
UUID(session_id)
start_event = "RUN_RESUMED" if command_type == "resume" else "RUN_STARTED"
publisher(start_event, {"session_id": session_id})
await publisher(start_event, {"session_id": session_id})
try:
if command_type == "resume":
tool_call_id = str(command.get("tool_call_id", ""))
if not tool_call_id:
raise ValueError("tool_call_id is required")
result = _run_async(
lambda: service_resume.resume(
session_id=session_id,
tool_call_id=tool_call_id,
)
result = await service_resume.resume(
session_id=session_id,
tool_call_id=tool_call_id,
)
else:
user_input = str(command.get("user_input", ""))
if not user_input:
raise ValueError("user_input is required")
result = _run_async(
lambda: service_run.run(
session_id=session_id,
user_input=user_input,
)
result = await service_run.run(
session_id=session_id,
user_input=user_input,
)
publisher("RUNTIME_EVENT", {"session_id": session_id, "result": result})
await publisher("RUNTIME_EVENT", {"session_id": session_id, "result": result})
extra_events = result.get("events") if isinstance(result, dict) else None
if isinstance(extra_events, list):
for event in extra_events:
@@ -140,8 +102,8 @@ def run_agent_task(
if not isinstance(event_type, str) or not isinstance(event_data, dict):
continue
payload = {"session_id": session_id, **event_data}
publisher(event_type, payload)
publisher("RUN_FINISHED", {"session_id": session_id})
await publisher(event_type, payload)
await publisher("RUN_FINISHED", {"session_id": session_id})
return result
except Exception: # noqa: BLE001
error_id = "agent_runtime_failed"
@@ -150,10 +112,19 @@ def run_agent_task(
session_id=session_id,
error_id=error_id,
)
publisher("RUN_ERROR", {"session_id": session_id, "error_id": error_id})
try:
await publisher(
"RUN_ERROR", {"session_id": session_id, "error_id": error_id}
)
except Exception as publish_exc: # noqa: BLE001
logger.warning(
"Failed to publish RUN_ERROR event",
session_id=session_id,
error=str(publish_exc),
)
raise
@celery_app.task(name="tasks.agent.run_command")
def run_command_task(command: dict[str, Any]) -> dict[str, object]:
return run_agent_task(command)
async def run_command_task(command: dict[str, Any]) -> dict[str, object]:
return await run_agent_task(command)
+21
View File
@@ -1,10 +1,29 @@
from __future__ import annotations
from celery import Celery
from celery import signals as celery_signals
from kombu import Queue
from core.config.settings import config
from core.logging import get_logger
from core.logging.celery import configure_celery_app
from services.base.redis import redis_service
logger = get_logger("core.celery")
def _init_redis_on_worker_startup(**_: object) -> None:
import asyncio
logger.info("Initializing Redis service for Celery worker")
try:
result = asyncio.run(redis_service.initialize())
if result:
logger.info("Redis service initialized for Celery worker")
else:
logger.warning("Redis service initialization returned False")
except Exception as exc: # noqa: BLE001
logger.error("Failed to initialize Redis for Celery worker", error=str(exc))
def create_celery_app() -> Celery:
@@ -50,3 +69,5 @@ def create_celery_app() -> Celery:
celery_app = create_celery_app()
celery_signals.worker_process_init.connect(_init_redis_on_worker_startup)
-7
View File
@@ -1,7 +0,0 @@
from __future__ import annotations
from pydantic import BaseModel
class HealthResponse(BaseModel):
status: str