# 修复 Celery Worker Redis Event Loop 冲突问题 **日期**: 2026-02-11 **类型**: Bug 修复 **影响范围**: 后端 - Celery 任务执行 ## 问题描述 Celery Worker 在执行 AI 任务时遇到 `RuntimeError: Event Loop is closed` 错误,导致任务失败。 ### 错误信息 ``` RuntimeError: Event loop is closed File "/app/app/tasks/ai_tasks.py", line 905, in _execute provider = await AIProviderFactory.create_provider(model, kwargs.get('config')) File "/app/app/services/ai_providers/factory.py", line 150, in create_provider raise ValueError ValueError: 模型 gpt-4o-mini 不存在或未激活 ``` ### 根本原因 1. **Celery 使用 ForkPoolWorker**:每个任务在独立的子进程中运行 2. **Event Loop 不共享**:主进程在 `worker_ready` 信号中创建的 Event Loop 无法在子进程中使用 3. **Redis 连接失效**:全局 Redis 连接(通过 `get_redis()` 获取)在子进程中不可用 4. **异步操作失败**:尝试在已关闭的 Event Loop 中执行异步 Redis 操作 ### 技术细节 ```python # ❌ 错误:使用全局 Redis 连接(在子进程中失效) async def get_cached_provider(cls, model_name: str): redis = await get_redis() # 全局单例,Event Loop 已关闭 cached_data = await redis.get(cls.CACHE_KEY) # RuntimeError ``` ## 修复内容 ### 修改文件 **文件**: `server/app/services/ai_providers/factory.py` **方法**: `AIProviderFactory.get_cached_provider` ### 修复方案 在每次调用时创建新的 Redis 连接,避免使用全局单例: ```python # ✅ 正确:在 Celery 任务中创建临时 Redis 连接 @classmethod async def get_cached_provider(cls, model_name: str) -> Optional[int]: """从 Redis 缓存获取 Provider ID(支持 Celery 任务)""" try: import redis.asyncio as redis from app.core.config import get_settings settings = get_settings() # 创建临时 Redis 客户端(不使用全局单例) redis_client = await redis.from_url( settings.REDIS_URL, encoding="utf-8", decode_responses=True ) try: cached_data = await redis_client.get(cls.CACHE_KEY) if not cached_data: logger.warning("⚠️ Redis 缓存为空,可能需要初始化") return None cache_dict = json.loads(cached_data) return cache_dict.get(model_name) finally: # 关闭临时连接 await redis_client.close() except Exception as e: logger.error("❌ 从 Redis 读取缓存失败: %s", e, exc_info=True) return None ``` ### 关键改进 1. **独立连接**:每次调用创建新的 Redis 连接,避免 Event Loop 冲突 2. **自动清理**:使用 `try...finally` 确保连接正确关闭 3. **错误处理**:捕获异常并返回 None,允许任务重试 ## 技术背景 ### Celery + asyncio 的挑战 Celery 默认使用 `prefork` 模式(ForkPoolWorker),每个任务在独立的子进程中运行: ``` 主进程 (MainProcess) ├─ worker_ready 信号 → 创建 Event Loop A └─ 子进程 (ForkPoolWorker-1) └─ 任务执行 → 尝试使用 Event Loop A(已关闭)❌ ``` ### 为什么全局 Redis 连接失败? 1. **Event Loop 绑定**:asyncio Redis 连接绑定到创建它的 Event Loop 2. **进程隔离**:子进程无法访问父进程的 Event Loop 3. **连接状态**:Redis 连接在子进程中处于"已关闭"状态 ### 解决方案对比 | 方案 | 优点 | 缺点 | 选择 | |------|------|------|------| | 临时连接 | 简单、可靠、无状态 | 每次创建连接有开销 | ✅ 采用 | | 同步 Redis | 避免 Event Loop 问题 | 需要重构大量代码 | ❌ | | 连接池 | 性能最优 | 复杂度高,需要管理生命周期 | ❌ | | Solo Worker | 避免进程问题 | 性能差,无法并发 | ❌ | ## 性能影响 ### 连接开销 - 每次任务创建一个 Redis 连接 - 连接时间:~10ms(本地网络) - 相比任务总时间(通常 >1s)可忽略 ### 优化建议 如果未来需要优化性能,可以考虑: 1. **连接池**:在每个 Worker 进程中维护独立的连接池 2. **缓存预热**:在 Worker 启动时将缓存加载到内存 3. **同步 Redis**:使用 `redis-py`(同步版本)避免 Event Loop 问题 ## 测试验证 ### 测试步骤 1. **重启 Celery Worker**: ```bash docker restart jointo-server-celery-ai ``` 2. **触发剧本解析任务**: ```bash POST /api/v1/screenplays/{screenplay_id}/parse ``` 3. **检查日志**: ```bash docker logs -f jointo-server-celery-ai | grep "Provider" ``` ### 预期结果 ``` [INFO] 创建 AIHubMix Provider: model=gpt-4o-mini [INFO] 剧本解析任务已提交: job_id=xxx ``` ### 错误日志(修复前) ``` [ERROR] 从 Redis 读取缓存失败: Event loop is closed [ERROR] 剧本解析任务失败: 模型 gpt-4o-mini 不存在或未激活 ``` ## 相关问题 ### 为什么不在 worker_ready 中修复? `worker_ready` 信号在主进程中执行,无法为子进程创建 Event Loop。Celery 的 `prefork` 模式要求每个子进程独立管理资源。 ### 其他地方是否有类似问题? 目前只有 `AIProviderFactory.get_cached_provider` 受影响,因为: - 其他 Redis 操作在 FastAPI 请求中执行(有独立的 Event Loop) - Celery 任务中的数据库操作使用 `get_async_session()`(每次创建新连接) ### 是否需要修改其他 Celery 任务? 不需要。其他任务(如导出任务)不使用 Redis 缓存,或者使用数据库连接(已正确处理)。 ## 相关文件 - `server/app/services/ai_providers/factory.py` - 修复 Redis 连接问题 - `server/app/core/celery_app.py` - Celery Worker 配置 - `server/app/core/cache.py` - Redis 缓存服务 - `server/app/tasks/ai_tasks.py` - AI 任务执行 ## 参考资料 - [Celery + asyncio 最佳实践](https://docs.celeryq.dev/en/stable/userguide/tasks.html#task-synchronous-subtasks) - [Redis asyncio 文档](https://redis.readthedocs.io/en/stable/examples/asyncio_examples.html) - [Python asyncio Event Loop 管理](https://docs.python.org/3/library/asyncio-eventloop.html)