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

3.9 KiB
Raw Blame History

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 语法检查、单元测试和回归测试。