Skip to content

fix(ui-e2e): studio-customize-journey reads open their own connection instead of a stale keep-alive socket - #30555

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-30524-studio-journey-reset
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-30524-studio-journey-reset

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

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 refactor(objectql): move 36 hand-series private docblocks out of engine.ts with the codemod's third pass (slice 1 of 2) #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.

Spec run, fixed head e8662a4, --repeat-each 12 (list lines condensed)
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)
Real backend, the spec's GET after an idle of N ms, 10 tries each
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}
Unfixed spec: reused-socket idle before each read (ms), full allowlist x3
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)

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

…ff the keep-alive pool

The spec's API reads are split by Studio steps the browser drives, and
Playwright's request context reuses a pooled keep-alive socket across
them. The read after the customize step reaches that socket after about
five seconds idle, against a Node 22 server that closes idle sockets at
six, so on a slower runner the read hits a socket the server is closing
and fails with read ECONNRESET. The spec's request fixture now sends
Connection: close, so every read opens its own connection. Every
assertion is unchanged.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 5, 2026
…request context by name

The spec's request fixture override built its context from extraHTTPHeaders
alone and relied on the runner filling in every option the call leaves out.
It now passes the project's request-relevant use options explicitly
(storageState, baseURL, userAgent, extraHTTPHeaders, ignoreHTTPSErrors),
merging Connection: close into any extra headers, so the reads run as the
signed-in admin session by declaration rather than by default.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#2 · marchtian · session_017ihL2ebbZkr2QAEG1x7UC8

席位核对 — ACCEPT · domain:devx#2 · Head-sha 22191f13b5dd1722807d70130f9a041d1195f76b · 2026-11-05T16:13Z

  • Shape: 1 file, ui/e2e/live/studio-customize-journey.spec.ts, +35/−1 (read from the diff, not the report). No governed path, under 3000 lines. The spec overrides its request fixture with a context that sends Connection: close, so each API read opens its own connection. Every assertion is unchanged. No retry, skip or timeout change.
  • Cause (dev report 6662128690): Playwright's request pools keep-alive sockets with no idle timeout of its own. The Node 22 backend (@hono/node-server) closes an idle socket at keepAliveTimeout + buffer = 6 s. The read after the customize step reused a socket idle for about 4.7–5.2 s. Against the real backend, a GET after a ~6 s idle failed 7 of 40 times; with Connection: close, 0 of 40. All three CI hits are this call right after that step. The backend pid was unchanged across them, so there was no restart.
  • Seat review rounds:
    1. The new context now passes storageState, baseURL, userAgent, extraHTTPHeaders and ignoreHTTPSErrors on by name (delta 6662646285). Trace: 170 of 170 requests carried the session cookie and Connection: close.
    2. Lint UI was red on a51f4c83a1: react-hooks/rules-of-hooks read the fixture callback use as React's hook. The seat renamed it provide (22191f13b5). Local eslint on the old text reproduced the error; on the new text it exits 0, and type-check:e2e exits 0.
  • Gates:
    • Head checks: 43 success and 12 skipped, including Live E2E UI and Lint UI.
    • check-expected-skips: OK, all 12 skips are in main's roster (exit 0).
    • check-governed-merges: 36 changed lines, which is ≤ 3000.
    • Dev's local dispatch-gates --commands: 19 of 19 exit 0, and --ran reconciled.
    • Spec runs: --repeat-each 10 --retries 0 passed 10 of 10, and an earlier --repeat-each 12 passed 12 of 12.
  • Not measured: the CI backend.log artifacts (the proxy refused the storage host). The no-restart reading rests on the job logs.
  • Noted, not carried: studio-editor and studio-object-designer reuse sockets after Studio steps at 3.5–4.1 s idle, under the 6 s close today. No card is filed; if they ever red with the same signature, this PR's fixture is the fix.
  • Out of scope: Skills-docs: n/a, Clause-②: no, Metadata-protocol: none.
  • Landing: skip-changeset (e2e spec only). Ready, then auto-merge through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 16:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 16:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 2ffdb64 Nov 5, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-30524-studio-journey-reset branch November 5, 2026 16:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

e2e: studio-customize-journey intermittently loses a meta read to ECONNRESET in Live E2E UI and kicks PRs out of the merge queue

1 participant