boardx / boardx/workspacex

fix(test-isolation): chat-read 的四个 provider 端口绕开 reserveIsolationPorts,是 EADDRINUSE 的真因

Open
#2,089 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 7m
Merged PRs (30d)
969

Description

从 #2084 的 39 份 `e2e-full` job 日志判读中分出来的独立红项。**这一条才是真正的「共享资源并发」**(#2086 那条不是端口,是共用账号)。

## 观测

39 个 run 里有 2 次 `EADDRINUSE`,两次都导致 webServer 起不来、**零个测试跑到**:

job 97796709558(`5cd37098`):
```
[test-isolation] id=run-mt8ntkxf-… compose=wsx-8dc689900610dffa8894 pg=23800 redis=28800
[WebServer] Error: listen EADDRINUSE: address already in use 127.0.0.1:54800
[WebServer] Emitted 'error' event on Server instance at: …
Error: Process from config.webServer was not able to start. Exit code: 1
```

job 97819094952(`0d660a87`):
```
[test-isolation] id=run-mt8qe4wb-… compose=wsx-5ba7469c60a853f8bda4 pg=24244 redis=29244
[WebServer] Error: listen EADDRINUSE: address already in use 127.0.0.1:59244
[WebServer] Emitted 'error' event on WebSocketServer instance at: …
```

## 关键:撞的**既不是 api 端口也不是 web 端口**

`apps/web/playwright.chat-read.config.ts` 用纯算术从 `WORKSPACEX_WEB_PORT` 又派生了四个端口:

```js
const modelProviderPort = String(Number(webPort) + 5_000); // :26
const deepAgentProviderPort = String(Number(webPort) + 6_000); // :32
const asrProviderPort = String(Number(webPort) + 10_000); // :38
const visionProviderPort = String(Number(webPort) + 14_000); // :50
```

反推:`pg=24244` ⇒ hash 偏移 4244 ⇒ `WORKSPACEX_WEB_PORT=49244` ⇒ ASR = 49244+10000 = **59244**,
且栈顶是 `WebSocketServer instance`——正是 `loopback-asr-provider.ts`。
另一次 `54800` 是 `Server instance` ⇒ webPort 49800 + 5000 = **model provider**。

## 为什么必然会撞

`.harness/scripts/lib/test-isolation.ts` 的 `reserveIsolationPorts()` 只 probe/持有**六个**键:
`PGPORT / REDIS_PORT / MINIO_PORT / MINIO_CONSOLE_PORT / WORKSPACEX_API_PORT / WORKSPACEX_WEB_PORT`。

上面那四个 provider 端口在 **playwright config 里下游算出来**,**从不 probe、从不持有、从不校验**,
完全在预留契约之外。而且它们的取值带互相重叠(web ∈ 45000–49999):

- model ∈ 50000–54999,deepAgent ∈ 51000–55999 → `webB = webA + 1000` 时 A 的 deepAgent = B 的 model
- asr ∈ 55000–59999,vision ∈ 59000–63999 → `webB = webA + 4000` 时 A 的 vision = B 的 asr

在共享 self-hosted runner 上,这在 5000 宽的带里是很普通的一次抽样。

## 修法方向

把这四个 provider 端口提升为 `reserveIsolationPorts()` 的一等键(各自独立、互不重叠的带),
而不是在 playwright config 里算。

⚠ 注意本仓纪律「加一层串行永远不够」:这里要修的是**没被隔离的那一层**(派生端口不在预留契约内),
不是再包一层锁或降并行度。

另有一处较小的 TOCTOU:即便那六个已预留的端口,`reservation.release()` 也发生在 `spawn()` 之前,
release 到 bind 之间有窗口。但那是窄窗口,而这四个派生端口是**完全无保护**,是本条的主因。

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in .harness/scripts/lib/test-isolation.ts and apps/web/playwright.chat-read.config.ts, tracing how reserved environment ports reach the chat-read providers. Run the relevant e2e-full isolation workflow before and after the change. Done means all four provider ports are independently reserved and consumed through the isolation contract, with concurrent runs no longer producing EADDRINUSE.

Written by the indexing model from the issue text.

Assessment

Tech stack
playwright, typescript
Domain
ci-cd, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.