Repository navigation
Commit 2ffdb64
fix(ui-e2e): studio-customize-journey reads open their own connection instead of a stale keep-alive socket (#30555)
<!-- claude-projects-attribution: {"github_login":"marchtian"} -->
_Requested by **田騏** · [project
thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiNjyvCdXYygFqntMNR7S2dW)_
Fixes #30524
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:`studio-customize-journey` 这条在线 e2e
的接口读取,每次都新开一条连接(`Connection: close`),不再复用空闲了几秒的旧连接;断言一条没动。
- **为什么改**:这条用例在合并队列里三次随机报 `read ECONNRESET`,把无关的绿 PR
踢出队列、拖累后面整队重跑。实测原因是测试客户端复用了一条快要被服务器按空闲超时(6 秒)关掉的连接,不是服务器出错。
- **风险与回滚**:只改一个测试文件,不发布任何包;每次读多一次本机建连,耗时可忽略。回滚即还原这一个文件。
- **先看**:`ui/e2e/live/studio-customize-journey.spec.ts` 顶部新增的 `request`
fixture 及其注释(唯一改动处)。
- **两分钟验证**:打开本 PR 的 `Live E2E UI` 检查,应为绿色,日志里
`studio-customize-journey.spec.ts` 一行为 ✓。
- **需要你做的**:无,席位落地。
## What changes
`ui/e2e/live/studio-customize-journey.spec.ts` overrides the `request`
fixture for this spec only: the context is
`playwright.request.newContext({ extraHTTPHeaders: { Connection: 'close'
} })`, so every API read the spec makes opens its
own connection and none rides a pooled keep-alive socket. The config's
`storageState` and other context options still
reach it (the runner fills every option the call does not name). The
browser's traffic is unchanged. Every assertion,
step and cleanup is unchanged.
## Cause (measured)
- Playwright 1.63's `APIRequestContext` pools sockets with `http.Agent({
keepAlive: true })` and no agent `timeout`.
With no agent timeout, Node's agent does not apply the server's
`Keep-Alive: timeout=5` hint, so a pooled socket is
kept until the server closes it.
- The backend (`@hono/node-server` on Node 22, default options) answers
`keep-alive: timeout=5` and closes an idle
socket at `keepAliveTimeout` 5000 + `keepAliveTimeoutBuffer` 1000 = 6000
ms (both read from
`http.createServer()` on Node 22.23.4).
- In this spec the read right after the customize step (`threeReads`,
the `servedColumns` GET) reuses the socket of
the read before the browser steps. A client-side socket trace measured
its idle at **4710–5153 ms** on 5 solo runs
and **4785–5010 ms** on 3 runs of the full CI allowlist. That is the
largest idle of any API read in the allowlist,
and it is about one second short of the server's close. A slower runner
pushes it onto 6 s.
- Against the real booted backend, the spec's own GET on a socket idled
5994–6000 ms: **7 of 40 failed** with
`apiRequestContext.get: read ECONNRESET` (3) or `socket hang up` (4).
With `Connection: close`: **0 of 40**.
- The three CI hits (PR #30341 run 41981408406 job 127092282802,
merge-group jobs 127279783258 and 127290410036) fail
at the same call, `servedColumns` from `threeReads` at spec line 159,
right after `publishPair`. The test dies at
6.5–6.6 s, 16 others pass, and the backend pid recorded at boot is the
pid stopped at the end. The next spec passes
within 4 s, while a backend boot takes 14 s. So nothing restarted the
server: one pooled socket was reset.
## Tests
The fixed spec ran 12/12 green against a stack booted the way the `Live
E2E UI` job boots it (start-backend.sh,
`vite preview` proxying `/api`, `--retries 0`). The derived gate
commands ran beside it, so the box was loaded. A socket
trace of the fixed spec shows **19 fresh connections and 0 reused**. The
unfixed spec showed 16 reused per run, the
largest idle 5153 ms.
<details><summary>Spec run, fixed head e8662a4, --repeat-each 12
(list lines condensed)</summary>
```
Running 12 tests using 1 worker
✓ 1 … studio-customize-journey.spec.ts:143:1 … (17.8s)
✓ 2 … (15.6s) ✓ 3 … (15.0s) ✓ 4 … (15.9s) ✓ 5 … (20.4s) ✓ 6 … (17.8s)
✓ 7 … (16.9s) ✓ 8 … (15.2s) ✓ 9 … (15.6s) ✓ 10 … (14.5s) ✓ 11 … (14.4s) ✓ 12 … (14.0s)
12 passed (3.4m)
```
</details>
<details><summary>Real backend, the spec's GET after an idle of N ms, 10
tries each</summary>
```
pooled (control)
idle 5994ms -> {"200":7,"apiRequestContext.get: socket hang up":2,"apiRequestContext.get: read ECONNRESET":1}
idle 5996ms -> {"200":6,"apiRequestContext.get: read ECONNRESET":2,"apiRequestContext.get: socket hang up":2}
idle 5998ms -> {"200":10}
idle 6000ms -> {"200":10}
Connection: close
idle 5994ms -> {"200":10}
idle 5996ms -> {"200":10}
idle 5998ms -> {"200":10}
idle 6000ms -> {"200":10}
```
</details>
<details><summary>Unfixed spec: reused-socket idle before each read
(ms), full allowlist x3</summary>
```
5010 GET /api/v1/meta/view/showcase_task.in_progress (journey, after customize)
4444 GET /api/v1/meta/object/showcase_task (journey, after the extension step)
4123 GET /api/v1/meta/page/e2e_studio_editor_… (studio-editor)
3537 GET /api/v1/meta/object/e2e_asset… (studio-object-designer)
4785 / 4420 / 4090 / 3538 · 4791 / 4219 / 4007 / 3498 (rounds 2 and 3, same order)
51 passed (2.9m)
```
</details>
## Acceptance notes
- The unfixed spec did not go red locally: 20/20 alone and 51/51 in the
allowlist x3. On this box the idle stays near
5 s, under the server's 6 s close. The failure was reproduced at the
socket level against the real backend (above).
- The CI artifacts' `backend.log` could not be read from the measuring
container: the artifact blob host is refused by
its egress proxy. The no-restart reading rests on the job logs: the same
pid at boot and at stop, and the next spec
passing.
- Other live specs make reads after Studio steps on a reused socket,
with a measured idle of 3.5–4.1 s
(`studio-editor`, `studio-object-designer`). They are under the 6 s
close today, so this PR leaves them unchanged.
carrier: none.
## Verification
- `node scripts/pm/dispatch-gates.mjs --commands` derived 19 commands
for this diff. All 19 ran locally, each exit code
captured before any pipe, and `--ran` reconciles them. The per-gate
readings are on the issue's `os-dev-report`
comment.
- No changeset: the diff touches only `ui/e2e/live/**`, which no
package's `files[]` ships (`ui/package.json` is the
private `object-ui-monorepo`), so the PR carries `skip-changeset`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0d54279 commit 2ffdb64
1 file changed
Lines changed: 35 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
46 | 80 | | |
47 | 81 | | |
48 | 82 | | |
| |||
0 commit comments