Files
cc-web/.trellis/tasks/09-27-codex-start-lock-retry/prd.md
2026-09-28 08:13:09 +08:00

36 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Codex 启动单实例锁与退避重试
## 目标
避免 cc-web 启动 Codex app-server 时与其他 cc-web 实例或残留启动流程并发写入同一 `CODEX_HOME`,导致 SQLite 报 `database is locked` 并直接启动失败。
## 范围
- 在 `lib/codex-app-server-client.js` 的 app-server 启动层实现跨进程单实例锁。
- 锁文件固定为 `<CODEX_HOME>/.cc-web-codex.lock`;锁目录按解析后的绝对 `CODEX_HOME` 隔离。
- 锁持有范围从启动尝试前开始,覆盖 app-server 初始化成功后的整个子进程生命周期;仅在子进程收到 `exit`/`error`、初始化失败清理完成或显式 `stop()` 后释放。
- 锁记录使用 JSON,至少包含 `pid`、Linux `/proc/<pid>/stat` 的进程启动时间(无法读取时为 `null`)、随机 `token`、创建时间。释放前必须校验 token,避免误删后来者的锁。
- 陈旧锁判定:PID 不存在,或 PID 存在但进程启动时间与记录不一致;记录损坏或不完整时,先按活动锁保护,超过 2 秒宽限期仍未补全才允许清理。
- 锁获取默认最多等待 30 秒,轮询间隔 200ms 起步、最多 2 秒并加入少量随机抖动;等待超时返回 `CODEX_HOME_LOCK_BUSY`,不杀死锁持有者。
- 对可判定为 SQLite/状态库暂时忙的初始化失败执行最多 4 次启动尝试,延迟为 500ms、1s、2s(上限 5s)并加入少量随机抖动;每次重试都等待旧子进程退出、释放本次锁后重新获取锁。默认总等待不超过 30 秒(不包含一次 30 秒初始化 RPC 超时)。
- 可通过环境变量覆盖:`CC_WEB_CODEX_LOCK_WAIT_MS`、`CC_WEB_CODEX_START_RETRY_ATTEMPTS`、`CC_WEB_CODEX_START_RETRY_DELAY_MS`、`CC_WEB_CODEX_START_RETRY_MAX_DELAY_MS`;所有值限制在合理范围内。
- 仅当 stderr、启动错误 message 或错误 cause 明确包含以下模式时重试:`database is locked`、`database table is locked`、`SQLITE_BUSY`、`SQLITE_BUSY_TIMEOUT`、`failed to open ... (state|log|database)`、`failed to initialize state runtime`。不因鉴权、命令不存在、参数错误、协议错误或普通网络错误重试。
- 保留现有 app-server 客户端 API 与 worker/source 两条启动路径。
- 增加可独立运行的 Node 回归测试,覆盖锁互斥、活动锁等待超时、陈旧锁清理、PID 启动时间不匹配保护、暂时性错误重试、非暂时错误不重试、成功/失败/显式停止后的锁释放。
## 约束
- 不删除用户数据,不改动用户的 Codex 数据库;只创建/清理 cc-web 自己的锁文件。
- 不把外部 Codex 进程误判为可安全杀死的残留进程;锁竞争只等待或报错。
- `SIGKILL` 等无法执行清理的场景依赖 PID + 启动时间识别陈旧锁,不保证实时删除锁文件。
- 启动失败最终抛出的错误保留最后一次原始 stderr,并附带尝试次数、锁路径和最后一次退避原因;日志不写入 API key。
- 重试仅发生在 `initialize` 阶段,不能自动重发 `thread/start`、`turn/start` 或其他有副作用的请求。
## 完成标准
1. 同一 `CODEX_HOME` 的两个 cc-web app-server 客户端不能同时持有锁;第二个客户端在锁释放前最多等待配置的锁等待时长,超时得到 `CODEX_HOME_LOCK_BUSY` 和锁持有者摘要。
2. 初始化 stderr/message 命中暂时性 SQLite 模式时,按明确的有限退避策略重新启动,成功后对调用方透明;非暂时性错误只尝试一次。
3. app-server 正常停止、初始化失败和进程退出都会释放锁;无存活 PID 或 PID 启动时间不匹配的陈旧锁不会永久阻塞启动,活动锁不会被误删。
4. 现有 Codex App 协议行为不变,worker/source 两条路径都经过同一锁与重试逻辑;语义判断器等额外客户端若无法获取锁,应收到可诊断的忙错误,不得杀掉已有 app-server。
5. 新增测试可稳定复现上述场景,并通过项目现有的 Node 语法检查、单元测试和回归测试。