codex@macbookpro
·
2026-03-27
BUG-012-browser-request-policy-waiter-deadlock.md
1# BUG-012: browser-request-policy waiter 无超时,slot 泄漏导致目标永久死锁
2
3## 状态
4
5- 已修复(2026-03-27,代码核对 + 自动化验证)
6
7## 当前代码结论
8
9- `apps/conductor-daemon/src/browser-request-policy.ts` 已新增 `BROWSER_REQUEST_WAITER_TIMEOUT_MS = 120_000`
10- target slot 和 platform admission 两类 waiter 都改为经由统一的 `waitForWaiter(...)` 等待,并在超时后从队列里移除自己
11- 超时会返回 `BrowserRequestPolicyError("waiter_timeout")`,并带 `wait_scope`、`client_id`、`platform`、`timeout_ms`
12- `apps/conductor-daemon/src/index.test.js` 已补 3 条专项测试:
13 - target slot waiter timeout
14 - platform admission waiter timeout
15 - HTTP `503 browser_risk_limited` 返回路径
16- 验证结果:
17 - `pnpm -C /Users/george/code/baa-conductor -F @baa-conductor/conductor-daemon build` 通过
18 - `node --test /Users/george/code/baa-conductor/apps/conductor-daemon/src/index.test.js` 通过(`31/31`)
19
20## 剩余风险
21
22- 这次修复解决的是“永久挂起”,不是“自动回收泄漏 slot”
23- 如果某个 lease 持续泄漏且一直不恢复,同一 `target` 的后续请求会稳定超时失败,而不是自动自愈
24- 如需继续增强,可后续新增 stale `inFlight` 清扫机制;这不属于本轮修复范围
25
26## 修复前现象
27
28当某个 browser request 的 policy lease 没有被正常 `complete()` 时(如 Firefox 断连、conductor 内部异常),对同一 `(clientId, platform)` 目标的所有后续请求永久挂起,表现为 `/v1/browser/request` 或 `/v1/browser/claude/send` 无响应。
29
30- 哪个模块:`apps/conductor-daemon/src/browser-request-policy.ts`
31- 返回了什么:无返回——请求永久等待
32- 预期:超时后应该返回错误或自动释放 slot
33- 是否稳定复现:需要特定异常路径触发,但一旦触发即永久生效
34
35## 修复前触发路径
36
37```text
38POST /v1/browser/request (或 /v1/browser/claude/send)
39-> beginBrowserRequestLease()
40-> BrowserRequestPolicyController.beginRequest()
41-> acquireTargetSlot() — 获得 slot,inFlight 变为 1
42-> admitRequest() — 等待限流/抖动
43-> 开始执行浏览器代发请求
44-> Firefox 插件 WS 断连 / conductor 内部 throw 未被 catch
45-> lease.complete() 从未被调用
46-> slot 永久被占用(inFlight 始终为 1)
47-> 后续请求进入 acquireTargetSlot() -> waiters.push(resolve) -> 永不 resolve
48```
49
50## 修复前根因
51
52两个独立问题共同导致:
53
54### 问题 1:acquireTargetSlot waiter 无超时
55
56```typescript
57private async acquireTargetSlot(state: BrowserRequestTargetState): Promise<void> {
58 if (state.inFlight < this.config.concurrency.maxInFlightPerClientPlatform
59 && state.waiters.length === 0) {
60 state.inFlight += 1;
61 return;
62 }
63 // 这个 Promise 永远没有 reject/timeout 机制
64 await new Promise<void>((resolve) => {
65 state.waiters.push(resolve);
66 });
67}
68```
69
70### 问题 2:acquirePlatformAdmission 同样无超时
71
72```typescript
73private async acquirePlatformAdmission(state: BrowserRequestPlatformState): Promise<void> {
74 if (!state.busy) {
75 state.busy = true;
76 return;
77 }
78 await new Promise<void>((resolve) => {
79 state.waiters.push(resolve);
80 });
81}
82```
83
84### 问题 3:lease 的 complete() 不保证被调用
85
86`BrowserRequestPolicyLeaseImpl.complete()` 由调用方手动调用,但如果调用方 throw 了且没有 finally 保护,complete 就不会被调用。
87
88## 复现步骤
89
90```bash
91TOKEN="<BAA_SHARED_TOKEN>"
92
93# 1. 发起一个 browser request
94curl -s https://conductor.makefile.so/v1/browser/claude/send \
95 -X POST -H "Content-Type: application/json" \
96 -H "Authorization: Bearer $TOKEN" \
97 -d '{"message":"test"}'
98
99# 2. 在请求执行过程中,断开 Firefox 插件的 WS 连接(关闭 Firefox 或重启插件)
100
101# 3. 如果 bridge error 的处理路径没有调 lease.complete(),slot 泄漏
102
103# 4. 后续对 claude 平台的所有 browser request 永久挂起
104curl -s https://conductor.makefile.so/v1/browser/claude/send \
105 -X POST -H "Content-Type: application/json" \
106 -H "Authorization: Bearer $TOKEN" \
107 -d '{"message":"this will hang forever"}'
108```
109
110注意:需要在精确的时间窗口内断开 Firefox 才能触发。但当前 `maxInFlightPerClientPlatform = 1`,意味着只需一次泄漏就会死锁。
111
112## 修复前影响
113
114- 当前是潜在风险,尚未在线上明确观察到
115- `maxInFlightPerClientPlatform = 1` 意味着一次泄漏 = 该 target 永久不可用
116- task scheduler 上线后,并发调用 browser request 的概率增大,触发风险提高
117- 重启 conductor-daemon 可恢复(内存状态清空)
118
119## 修复方案(已落地)
120
121### 方案 A(推荐:多层防御)
122
123同时修三个问题:
124
125**A1. waiter 加超时(120s)**
126
127给 `acquireTargetSlot` 和 `acquirePlatformAdmission` 的 waiter Promise 加超时,超时后从 waiters 数组中移除自己,reject 为 `BrowserRequestPolicyError("slot_wait_timeout")`。
128
129**A2. lease 加 safety net**
130
131在 `executeBrowserRequest`(local-api.ts)中确保所有退出路径都调 `lease.complete()`,使用 try/finally 包裹。
132
133**A3. slot 泄漏自愈检测**
134
135定期(例如每 60s)扫描 targets Map,如果某个 target 的 `inFlight > 0` 但最后一次 admit 已超过 5 分钟,强制重置 `inFlight = 0` 并释放一个 waiter。
136
137### 方案 B
138
139只做 A2(保证 finally 调 complete),不改 waiter 超时。更简单但防御层次少。
140
141## 严重程度
142
143Medium
144
145- 当前不常触发,但一旦触发即永久死锁(直到重启)
146- task scheduler 上线后风险升高
147- `maxInFlightPerClientPlatform = 1` 放大了影响——零容错
148
149## 发现时间
150
1512026-03-26 by Claude (code review)
152
153## 备注
154
155- 当前文档保留为问题归档;实际代码已完成 waiter timeout 与清理
156- 本轮没有实现 stale `inFlight` 自动清扫;这是后续增强项,不是未修复状态