Commit f187423
authored
feat(proxy): WebSocket connection pool fixes prompt-cache hit-rate jitter (#440)
* feat(proxy): add WS connection pool module (no wiring yet)
新增 PersistentWs + WsConnectionPool + WsReusedConnectionError,准备解决
上游 WS gateway 按连接 ID 哈希路由导致的 prompt cache 命中率抖动
(5%~99% bimodal)。本 commit 纯模块新增 + 26 个单测,未接线,零回归风险。
设计要点:
- Pool key = `${entryId}:${conversationId}`,覆盖显式 + 隐式续链
- 单 WS strict 串行(codex 协议要求),busy 时 acquire 返回 bypass 让 caller
走旧路径(不排队,避免死锁)
- 无 idle TTL;max_age=55min 留 5min 缓冲(server 60min 硬限)
- 死连/abort/账号状态变化级联清理
- 复用失败(pre-response)抛 WsReusedConnectionError,caller 可单次 retry;
流中段失败走 controller.error 不 retry(client 已收到部分数据)
config schema 新增 ws_pool: { enabled: true, max_age_ms, max_per_account }
* feat(proxy): wire WS connection pool through ws-transport + proxy-handler
接线 ws-pool 到主请求链路。
ws-transport.ts:
- 抽出 openOneShotWs 保持原有 one-shot 语义(向后兼容,旧调用方零变更)
- createWebSocketResponse 增加 poolCtx? 参数;带 ctx 时先尝试 pool.acquire
→ 命中复用 PersistentWs.send;遇 WsReusedConnectionError 单次回退 one-shot
- pool 自身故障(factory 抛错)也会 fallback 到 one-shot,不污染调用方
codex-api.ts:
- createResponse / createResponseViaWebSocket 透传 poolCtx 到 createWebSocketResponse
- HTTP 路径完全不受影响
proxy-handler.ts:
- buildPoolCtx() 根据 useWebSocket + chainConversationId 生成 poolKey
= `${entryId}:${chainConversationId}`,仅 WS 路径生效
- 主流程 + handleNonStreaming 的 empty-response retry 都用同一个 builder
account-pool.ts:
- markStatus(non-active) / markRateLimited / removeAccount / updateToken(refresh
完成)级联调 evictByEntryId 关闭该账号所有池中 WS。理由:refresh 后老 WS
携带的 access_token 已失效;其他状态变化下账号本身不可用,留池只浪费
- 用 dynamic import 隔离 ws-pool,避免 account-pool 单测必须拉 proxy 层
src/index.ts: SIGINT/SIGTERM shutdown 钩子追加 wsPool.shutdown() 优雅关闭
集成测:tests/integration/ws-pool-reuse.test.ts 起本地 ws.Server 验证
- 5 turn 同 conv → server 仅 1 次 connection
- 不同 conv → 各自 1 个连接
- server 主动 close → 池立即驱逐,下次 acquire 新建
- enabled:false → 退化为 one-shot(每 turn 新连接)
- evictByEntryId → 池清空 + 后续重建
- 不传 poolCtx → 行为完全等价于今天
测试:1702 passed (+1 skipped),零回归
* feat(proxy): observability for WS pool decisions + CHANGELOG
ws-transport: 新增 WsDispatchDecision 类型 + WsPoolContext.onDecision 回调,
四种决定 (reuse / new / bypass:<reason> / retry-after-stale-reuse) 在 dispatch
时刻一次性 emit 给 caller。
proxy-handler: buildPoolCtx 装上 onDecision listener,对每个 WS 请求 emit
一行 `[fmt] Account E | rid=R | ws=reuse:abc` 之类的日志,便于直接观察
池命中率 + 抖动归因。
CHANGELOG: Unreleased → Fixed 加完整条目,写明问题、原因、配置、回滚。
* fix(proxy): wire ws_pool config into singleton (self-review of #440)
PR #440 加了 config schema 字段 ws_pool 但忘了真正读取它 —— `getWsPool()`
永远用 DEFAULT_WS_POOL_CONFIG,用户改 `ws_pool.enabled: false` 完全无效,
回滚策略破坏。
修复:startServer() 在加载 cfg 后立即调 setWsPoolConfig() 用用户配置替换
单例。同时把 shutdown 钩子的 dynamic import 一起改成 static(同模块复用)。
新增 3 个测试覆盖单例 wiring 链路:
- enabled:false → acquire 永远 bypass(disabled),factory 不调
- 默认 getWsPool() 工作正常
- setWsPoolConfig 后调覆盖前调
测试:1705 passed (+1 skipped)
---------
Co-authored-by: icebear0828 <icebear0828@users.noreply.github.com>1 parent f62a4ac commit f187423
11 files changed
Lines changed: 1678 additions & 17 deletions
File tree
- src
- auth
- proxy
- tests
- _helpers
- integration
- unit/proxy
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
18 | 30 | | |
19 | 31 | | |
20 | 32 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| 117 | + | |
117 | 118 | | |
118 | 119 | | |
119 | 120 | | |
120 | 121 | | |
121 | 122 | | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
122 | 141 | | |
123 | 142 | | |
124 | 143 | | |
| |||
135 | 154 | | |
136 | 155 | | |
137 | 156 | | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
138 | 161 | | |
139 | 162 | | |
140 | 163 | | |
| |||
147 | 170 | | |
148 | 171 | | |
149 | 172 | | |
| 173 | + | |
150 | 174 | | |
151 | 175 | | |
152 | 176 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
72 | 72 | | |
73 | 73 | | |
74 | 74 | | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
75 | 87 | | |
76 | 88 | | |
77 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
21 | 21 | | |
22 | 22 | | |
23 | 23 | | |
| 24 | + | |
24 | 25 | | |
25 | 26 | | |
26 | 27 | | |
| |||
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
113 | 123 | | |
114 | 124 | | |
115 | 125 | | |
| |||
299 | 309 | | |
300 | 310 | | |
301 | 311 | | |
302 | | - | |
| 312 | + | |
303 | 313 | | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
304 | 317 | | |
305 | 318 | | |
306 | 319 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | 20 | | |
| 21 | + | |
| 22 | + | |
21 | 23 | | |
22 | 24 | | |
23 | 25 | | |
| |||
175 | 177 | | |
176 | 178 | | |
177 | 179 | | |
| 180 | + | |
178 | 181 | | |
179 | 182 | | |
180 | 183 | | |
181 | | - | |
| 184 | + | |
182 | 185 | | |
183 | 186 | | |
184 | 187 | | |
| |||
211 | 214 | | |
212 | 215 | | |
213 | 216 | | |
| 217 | + | |
214 | 218 | | |
215 | 219 | | |
216 | 220 | | |
| |||
246 | 250 | | |
247 | 251 | | |
248 | 252 | | |
249 | | - | |
| 253 | + | |
250 | 254 | | |
251 | 255 | | |
252 | 256 | | |
| |||
0 commit comments