bug(agent): CreateRunRequest.idempotency_key is accepted but not enforced

该报错通常出现在自托管 dify-agent run server 接入 Redis 后,调用方在 POST /runs 中传入 idempotency_key 并重放同一请求(或并发请求打到共享同一 Redis 前缀的不同进程)时发生。优先排查 RunScheduler.create_run()

快速结论:该报错通常出现在自托管 dify-agent run server 接入 Redis 后,调用方在 POST /runs 中传入 idempotency_key 并重放同一请求(或并发请求打到共享同一 Redis 前缀的不同进程)时发生。优先排查 RunScheduler.create_run() 是否在 run 准入阶段原子占用幂等键,而不是在写入 run 记录之后才登记键。

适用环境:Issue 已确认:Dify main 分支(base commit 59fb603)、Self Hosted (Source)、独立运行的 dify-agent run server、依赖 Redis。Python、CUDA、PyTorch、显卡等未在 Issue 中给出,不做推断。

最快修复方案:暂无确认的一步修复方案。Issue 中提出的方向是:在 Redis 中用 SET key NX PX/EX <retention> 原子认领幂等键,只有认领成功的一方生成 run id 并创建 run,认领失败的一方返回原 run id 与当前状态。

注意事项:该方案属于 Issue 中提出的设计,未在 59fb603 上验证;评论中讨论的并发压测数字来自评论者自己的复现场景,并非 Dify 调度器的实测结果。对外可见行为变更(同一键不同请求应返回 HTTP 409)需要回归测试覆盖,且不带幂等键的请求应保持原有行为。

问题场景

自托管部署下启动独立的 dify-agent run server 并配好 Redis,向 POST /runs 发送带 idempotency_key 的请求,然后用相同的键重放同一请求。预期是幂等去重,实际观察到服务器生成了新的 run id 并启动了新的后台任务。当并发请求分别落到共享同一 Redis 前缀的不同 server 进程时,也会出现同样现象。

报错原文

bug(agent): CreateRunRequest.idempotency_key is accepted but not enforced

RunScheduler.create_run() always creates a new UUID, persists a new run record,
and starts a new task. CreateRunRequest.idempotency_key is accepted but is not
used during run admission.

原因分析

已确认的原因是:CreateRunRequest.idempotency_key 只在请求模型中被接收,run 准入路径并未使用它;RunScheduler.create_run() 每次都生成新 UUID、持久化新 run 记录并启动新任务。

评论补充了两类可能原因,可作为实现排错时的关注点:

  • 若采用先查询再写入(check-then-act)的方式实现去重,两个并发重放可能同时查不到记录,从而各自创建 run,仍然无法满足 at-most-once 的要求。
  • 若幂等键是在 run 记录写入之后(或经过较慢的校验步骤之后)才登记,这段窗口内后来者会看到“尚未处理”而重复执行。
  • 若状态查询或监控路径也会读写幂等记录,可能误“消耗”该键,导致后续真实请求被误判为重复。

环境排查

  • 确认 Dify 版本与分支,Issue 对应 main 分支 base commit 59fb603,排查前先核对是否包含相关修复。
  • 确认是否为 Self Hosted (Source) 部署,以及 dify-agent run server 是否独立启动。
  • 确认 run server 已正确连接 Redis,并核对多个 server 进程使用的 Redis prefix 是否一致。
  • 确认 POST /runs 请求中确实携带了 idempotency_key,并记录重放时请求体是否完全一致。
  • 确认 Agent v2 workflow、retry、cleanup 调用方是否已在生成幂等键,以便区分是调用方未传键还是服务端未生效。

解决步骤

  1. 在 run 准入路径(RunScheduler.create_run() 入口)加入幂等处理,而不是在 run 记录落库之后再补登记键。
  2. 可优先尝试评论中的原子认领思路:以 SET run:idem:<prefix>:<key> <new_run_id> NX EX <retention_window> 作为认领操作。返回 OK 表示本请求负责创建该 run;返回 nil 则读取已有键,返回原 run id 及当前状态。
  3. 为幂等键关联规范化请求指纹(如哈希摘要),仅持久化摘要和 run id,不存储模型凭证或原始请求。
  4. 同一键配合不同规范化请求时返回 HTTP 409,避免静默返回不匹配的原 run。
  5. 保证查询/监控等只读路径读取幂等记录时不执行认领操作,仅准入路径可以认领。
  6. 不带幂等键的请求保持原有行为,即每次创建新 run。
  7. 补充回归测试,至少覆盖:完全相同的重放、并发重放、同键不同请求体、无键请求。

验证方法

在配置的运行保留窗口内,用同一幂等键和同一请求体重放 POST /runs,应返回原 run id 及当前状态,且服务端没有新增后台任务;用同一键发送不同规范化请求,应返回 HTTP 409;不带幂等键的请求仍应创建新 run。多进程并发重放同一键时,只应产生一次 run 创建。若按上述步骤实现,应同时确认只读查询路径不会消耗幂等键。

参考来源

langgenius/dify #39685

GamsGo AI

AI 工具推荐

想把多个 AI 模型放在一个入口?

GamsGo AI 集成 ChatGPT、DeepSeek、Gemini、Claude、Midjourney、Veo 等常用模型,适合写作、绘图、视频和日常 AI 工作流。

了解 GamsGo AI

推广链接:通过此链接购买,我可能获得佣金,不影响你的价格。

这个方案解决了吗?

celebrityanime
celebrityanime
文章: 34867

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注