fix(agent): enforce idempotency_key during create-run admission - #39692
Open
ccl125 wants to merge 1 commit into
Open
fix(agent): enforce idempotency_key during create-run admission#39692ccl125 wants to merge 1 commit into
ccl125 wants to merge 1 commit into
Conversation
The standalone dify-agent run server accepted CreateRunRequest.idempotency_key but never consulted it: RunScheduler.create_run() always persisted a new run record and started another background task, so retries and concurrent duplicate requests repeated model invocations and tool side effects. Deduplicate admission in RedisRunStore behind an atomic SET NX claim keyed by the digest of the idempotency key, storing only the request fingerprint digest and the claimed run id (never the create-run payload, which may carry model credentials). Within the run retention window an exact replay now returns the original run id and current status without scheduling a second task, reusing the key with a different normalized request is rejected with HTTP 409, and requests without a key keep the existing behavior.
📄 Knowledge review✏️ Suggested updates1 page suggestion needs review.
📝 Dify Agent Server 模块分析 (Commit 55f95db)@@ -161,6 +161,7 @@
- **Run 记录**:以 JSON 字符串存储在 Redis Key 中,记录 `run_id`、`status`、`created_at`、`updated_at`、`error`
- **事件流**:每个 run 对应一条 Redis Stream,事件以 `xadd` 追加写入
- **TTL 刷新**:每次写入状态或事件时,同步刷新 run 记录和事件流的过期时间(默认保留 3 天)
+- **幂等键声明**:用于去重的幂等键(`idempotency_key`)以其摘要为键存储在 Redis 中,TTL 与 run 保留窗口一致。仅持久化键摘要、请求指纹摘要和声明的 run ID,从不存储创建 run 负载(可能包含模型凭证)
Redis Stream ID 作为公开的事件游标,`0-0` 表示从头回放,支持断点续传。
@@ -184,8 +185,10 @@
- 检查 `stopping` 标志
- 执行 run 请求验证
-- 持久化 run 记录
+- 持久化 run 记录(或幂等键声明)
- 注册后台任务
+
+当请求携带 `idempotency_key` 时,调度器通过 `RedisRunStore.create_run_idempotent()` 执行幂等准入控制:计算请求负载的稳定指纹(排除幂等键本身),声明幂等键,并在保留窗口内的精确重放返回原始 run 而不启动第二个后台任务。
### FastAPI Lifespan 生命周期
@@ -576,7 +579,7 @@
| 方法 | 路径 | 功能 | 状态码 |
|---|---|---|---|
-| `POST` | `/runs` | 创建并调度一个 Agent 运行 | 202 / 422 / 503 |
+| `POST` | `/runs` | 创建并调度一个 Agent 运行 | 202 / 409 / 422 / 503 |
| `GET` | `/runs/{run_id}` | 查询运行状态 | 200 / 404 |
| `GET` | `/runs/{run_id}/events` | 轮询事件列表(游标分页)| 200 / 404 |
| `GET` | `/runs/{run_id}/events/sse` | SSE 事件流(实时推送)| 200 / 404 |
@@ -598,9 +601,12 @@
]
},
"session_snapshot": null,
- "on_exit": { "default": "suspend", "layers": {} }
+ "on_exit": { "default": "suspend", "layers": {} },
+ "idempotency_key": "unique-request-identifier"
}
```
+
+可选的 `idempotency_key` 字段用于防止客户端重试和并发重复请求重复模型调用和工具副作用。在保留窗口内使用相同键的精确重放返回原始 run;使用相同键但不同请求负载返回 HTTP 409。
**响应体(202 Accepted)**:`CreateRunResponse`
@@ -1135,7 +1141,7 @@
| 环境变量 | 默认值 | 说明 |
|---|---|---|
-| `DIFY_AGENT_REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 URL,用于存储运行记录和事件流 |
+| `DIFY_AGENT_REDIS_URL` | `redis://localhost:6379/0` | Redis 连接 URL,用于存储运行记录、事件流和幂等键声明 |
| `DIFY_AGENT_REDIS_PREFIX` | `dify-agent` | Redis 键名前缀,用于多实例部署时的命名空间隔离 |
#### 关闭与数据保留 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #39685
The standalone dify-agent run server accepted
CreateRunRequest.idempotency_keybut never used it during run admission:RunScheduler.create_run()always persisted a new run record and started another background task, so client retries (and concurrent duplicate requests hitting different server processes sharing the same Redis prefix) repeated model invocations and tool side effects.Changes
RedisRunStore.create_run_idempotent(): claims the idempotency key behind an atomicSET NX(TTL = run retention window), keyed by the key digest. Only the request fingerprint digest and the claimed run id are persisted — never the create-run payload, which may carry model credentials. A stale claim whose original run record is already gone is taken over by the fresh run.RunScheduler.create_run(): when the request carries anidempotency_key, computes a stable fingerprint over the normalized request (excluding the key itself) and deduplicates admission. An exact replay within the retention window returns the original run id and current status without scheduling a second task.POST /runs: reusing the key with a different normalized request returns HTTP 409.Testing
pytest tests— 649 passed; the 2 failures intest_run_scheduler.pyand the 4 collection errors from duplicate test-module basenames also reproduce on a cleanmaincheckout and are unrelated to this change.ruff check,ruff format --check, andbasedpyright --level errorare clean on the touched files.