3.9 KiB
3.9 KiB
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或其他有副作用的请求。
完成标准
- 同一
CODEX_HOME的两个 cc-web app-server 客户端不能同时持有锁;第二个客户端在锁释放前最多等待配置的锁等待时长,超时得到CODEX_HOME_LOCK_BUSY和锁持有者摘要。 - 初始化 stderr/message 命中暂时性 SQLite 模式时,按明确的有限退避策略重新启动,成功后对调用方透明;非暂时性错误只尝试一次。
- app-server 正常停止、初始化失败和进程退出都会释放锁;无存活 PID 或 PID 启动时间不匹配的陈旧锁不会永久阻塞启动,活动锁不会被误删。
- 现有 Codex App 协议行为不变,worker/source 两条路径都经过同一锁与重试逻辑;语义判断器等额外客户端若无法获取锁,应收到可诊断的忙错误,不得杀掉已有 app-server。
- 新增测试可稳定复现上述场景,并通过项目现有的 Node 语法检查、单元测试和回归测试。