Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fix(console): #274 并发同 dedup_key 双 POST 各自入队——预认领与占位行非原子,落败 id 永久 queued 且占配额

Open
#280 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@okguitar is already working on this.

Since Oct 21, 2026.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
python, redis

Research direction

Start with POST /api/executions and _claim_start_dedup_at_post, then trace _write_queued_placeholder, _handle_start_dedup_hit, and the worker start flow. Read the race probe at /tmp/r101_dedup_race.py and reproduce the interleaving with its placeholder-write barrier. Done means the acceptance checks pass: concurrent same-tenant requests share one id and queue entry, crashed claims can be recovered, and the existing TTL and tenant-isolation behavior remains intact.

Written by the indexing model from the issue text.

Description

优先级 P1 · 依赖:无

基线 HEAD = 280be04(含 #273/#274)。探针 clean archive:/tmp/plaita-r101-tree。

Summary

#274 的 console 侧预认领不是原子的:POST /api/executions 的流程是

  1. 铸造新 execution_id;
  2. _claim_start_dedup_at_post 对 {ns}:start-dedup:{dedup_key} 做 SET NX;
  3. 命中时读 mapped id 的执行行;行不存在则视为「孤儿映射」,_release_start_dedup_if_owner 删除后重新认领;
  4. 才写 queued 占位行并入队。

当两个同 dedup_key 的请求并发到达时,A 已 SET NX 成功但尚未写占位行,B 命中映射后发现行不存在,把 A 的映射当孤儿删掉并用自己的 id 重新认领。随后 A、B 都继续写占位行并各自入队。结果是同一个 dedup_key 返回两个不同 execution_id,队列里有两条 start 载体。

复现探针:/tmp/r101_dedup_race.py(fakeredis + TestClient;用 _write_queued_placeholder 的屏障精确卡在 A 的「预认领成功、占位行未写」窗口):

B result: 200 {'status':'queued','execution_id':'29cdbfb28ecc4836b1dd766db8937723',...}
A result: 200 {'status':'queued','execution_id':'3aed66b11a21429c8bb144974e574a50',...}
queued start ids: ['29cd...', '3aed...']   distinct = 2
dedup key final: 29cd...
state rows: [('29cd...', True, 'queued'), ('3aed...', True, 'queued')]
RACE REPRODUCED

影响

  • 幂等契约失效:并发重试(客户端超时后重发、双副本同时转发、消息重投)是同 dedup_key 最需要收敛的场景,却直接返回两个 id;两条响应都声称自己已受理。
  • 落败 id 永久 queued:worker 消费落败载体时 _claim_start_dedup 返回 hit,_handle_start_dedup_hit 读到 mapped 行是 queued,最终返回 {status: queued, deduplicated: True};start_flow 直接返回,run() 正常 XACK 这条 start 载体。落败 id 的 queued 占位行没有任何写者再推进,也没有终态 TTL(非终态不设 TTL),GET/列表永久显示 queued。
  • 配额泄漏:两个 POST 都在 try_admit_tenant_start 各占一个名额;落败载体的 worker hit 路径按 mapped id 返回,不会释放落败 id 的配额(终态释放收口只认 state.execution_id)。该名额占用到配额条目 TTL 才自愈,期间同租户可被虚假占满。
  • 队列重复载体:落败 start 会被消费并 ack,不产生第二次副作用(worker 映射收敛到胜者),但用户侧看到的是两条「已受理」执行,排障/取消/配额统计都指向错误 id。

建议

预认领必须与「占位行已可读」建立原子/可等待的关系,例如:

  1. 用 Lua 同时完成 SET NX + 写一个最小 claim 记录(或 SET NX 的 value 带状态),命中方看到 claim 但行未落时等待/轮询,不得把在途认领当孤儿删除;
  2. 或把预认领改成 SET NX 后由 owner 写占位行,命中方仅在确认 owner 已死亡(短 TTL claim + heartbeat)后才接管,且接管本身用 CAS(GET owner == mapped 才 DEL);
  3. 孤儿判定必须能区分「owner 正在写」与「owner 已崩溃」;当前「行不存在即孤儿」的判据在并发窗口内不成立。

验收:

  1. 两个同租户同 dedup_key 的并发 POST:两个响应 execution_id 相同,队列只有一条 start,只有一个 queued 占位行;
  2. 若一个请求在 SET NX 后崩溃且未写行:后续重试能在有限等待/接管后成功,不得永久 404 或永久 queued;
  3. force DELETE 后 7 天内用同 dedup_key 重试仍按新 id 启动(#274 既有验收保持);
  4. 不同租户同 key、不同 key 的隔离不变。

边界

  • 本项不涉及跨租户;租户键命名空间隔离在顺序/并发场景均保持。
  • 不改变 worker 7 天 dedup TTL 与「同键必同 id」契约,只修生产侧并发认领的原子性。
Dominant language
Python
Stars
0
Forks
1
Avg merge
2h 22m
Merged PRs (30d)
8

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from jeffkit/plaita

All issues in jeffkit/plaita

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.