核心架构变化:手写异步队列改为成熟任务队列库;arq 简介

核心架构变化:手写异步队列改为成熟任务队列库

删除了自建的 TaskDispatcherService(asyncio.Queue + worker 协程 + 手写状态机),迁移到 arq(基于 Redis 的成熟任务队列库)。

为什么做这个决定

  • 自建 dispatcher 要手写取消功能:区分"排队中"和"正在执行"两种状态,
    前者要维护 _canceled_pending 集合,后者要维护 session_id -> asyncio.Task
    映射并调用 task.cancel(),代码量和心智负担都不小。
  • arq 原生支持 job.abort(),取消逻辑不用自己写。
  • arq 是 pydantic/FastAPI 同一作者写的,API 是 async 原生风格,跟现有代码风格契合。

⚠️ 阻断项:InMemorySaver 在多进程/多次初始化下会失效

问题本质

@lru_cache(maxsize=1)
def build_adaptive_rag_graph():
    ...
    return g.compile(checkpointer=InMemorySaver())

InMemorySaver 的状态只存在于当前进程的内存里。之前 dispatcher 和 FastAPI
在同一个进程,lru_cache 保证全进程共享同一份 checkpoint 数据。

如果 arq worker 是独立进程跑(arq app.worker.WorkerSettings),worker 进程和
API 进程是两个独立的 Python 解释器,各自的 InMemorySaver 互不相通——worker 写入的
状态,API 那边的 /result 永远查不到。

结论

必须换成跨进程共享的 checkpointer(如 AsyncPostgresSaverAsyncRedisSaver),
这一步还没有完成,是上线前必须解决的阻断项,不是可以后延的优化。

部署模式:arq worker 内嵌到 FastAPI 同进程

选择了在 FastAPI 的 lifespan 里直接用 arq.worker.Worker 构造并 async_run()
而不是让 arq worker 作为独立进程/Supervisor 管理的服务运行。

取舍

  • 好处:省一个部署进程,本地开发和小规模单机部署更省心。
  • 代价:失去了"横向扩展 worker 数量"和"API 崩了 worker 还能继续跑"这两个独立
    进程模式自带的优势。以后如果要用 uvicorn --workers N 起多进程,每个进程都会
    各自起一个内嵌 worker,max_jobs 会被放大 N 倍,需要注意别重复扩大并发。

arq 简介

arq 是一个基于 Redis 的异步任务队列库,专为 Python 的 asyncio 设计

特点

  • 任务函数就是普通的 async def
  • 只依赖 Redis,不需要 RabbitMQ 等额外中间件
  • 内置支持:任务超时、取消、重试、定时任务(cron)、结果过期

基本用法

# 定义任务
async def my_task(ctx: dict, x: int) -> int:
    return x * 2

# worker 配置
from arq.connections import RedisSettings

class WorkerSettings:
    functions = [my_task]
    redis_settings = RedisSettings(host="localhost", port=6379)

# 启动
# 命令行:arq app.worker.WorkerSettings
# 入队任务
job = await redis.enqueue_job("my_task", 5)

# 查状态
from arq.jobs import Job
status = await Job(job.job_id, redis).status()

# 取消
await Job(job.job_id, redis).abort()

和 Celery 比

  • Celery:生态更大更成熟,但异步支持是后加的,不如 arq 原生
  • arq:更轻量,专门给纯 asyncio 项目用,够用就好
posted @ 2026-07-06 17:51  asphyxiasea  阅读(5)  评论(0)    收藏  举报