Skip to content

Commit ababe91

Browse files
sametbasbugclaude
andcommitted
Stop two test workers from being handed the same port
Gecelik regresyon 13 Ağustos'ta kodla ilgisi olmayan bir sebeple kırmızı oldu: workerd `Address already in use (127.0.0.1:32829)` diyerek öldü, arkasından üç süit ECONNREFUSED çığına döndü. Aynı commit ikinci denemede baştan sona yeşil geçti. Sebep port ayırıcısıydı. Yedi D1 test dosyası kendi `wrangler dev`'ini başlatıyor, `test:d1` onları beşerli koşuyor ve her dosya portunu şöyle seçiyordu: 0'a bağlan, verilen portu oku, soketi kapat, wrangler'ı o portla başlat. Soketi kapatmakla wrangler'ın bağlamasi arasında saniyeler var ve o aralıkta port işletim sistemi için boş — ikinci bir süreç sorarsa aynı portu alabiliyor. Portu soket kapandıktan sonra da rezerve tutuyoruz. Süreçler ayrı olduğu için rezervasyon süreç içi bir küme olamaz; `open(..., 'wx')` ile dosya sisteminde duruyor, çekirdek düzeyinde atomik. İşaret sahibinin pid'ini taşıyor: çöken bir koşunun bıraktığı işaret, sahibi ölmüşse geri alınıyor — yoksa düzeltme zamanla kendi arızasına dönerdi. Yedi kopya yardımcı fonksiyon tek modüle indi ve her çağıranın kendi yazdığı "aynı portu iki kez alma" kontrolü de onunla birlikte gitti. Ölçüm konusunda dürüst olmak gerekiyor: eşzamanlılık testi bir gerilemeyi şanstan yakalar. Eski ayırıcıyla aynı deneyi macOS'ta 40 tur koşturdum, tek çakışma çıkmadı — pencere dar ve işletim sistemi portları sırayla veriyor; Linux runner'da bir kez çakıştığını biliyoruz çünkü olmuş. Asıl kanıt diğer iki testte: tutulan bir port ikinci kez dağıtılmıyor, ölmüş bir sürecin bıraktığı rezervasyon geri alınıyor. İkisi de determinist. Yerel kanıt: `npm run test:d1` iki kez uçtan uca, 269 test, 0 hata; iki koşudan sonra rezervasyon dizini boş, yani çıkış kancası işini yapıyor. `npm run check` 0 hata, `production:config:check` 85 iddia, `actions:scope:test` yeni dosyayı tam bir bölünmede görüyor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 42107f2 commit ababe91

10 files changed

Lines changed: 272 additions & 129 deletions

‎package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
"staging:slice5:images-cpu": "tsx scripts/orbit-slice5-images-cpu-staging-e2e.ts",
3636
"orbit:import:verify": "tsx scripts/orbit-slice3-manifest.ts",
3737
"orbit:import:local": "tsx scripts/orbit-slice3-import.ts --local --database=orbit-v6-local --config=wrangler.test.jsonc",
38-
"test:d1": "tsx --test --test-concurrency=5 scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-site-authorization-tests.ts scripts/orbit-site-signin-e2e-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-single-renderer-tests.ts scripts/orbit-style-scale-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts scripts/orbit-slice4-tests.ts scripts/orbit-slice5-tests.ts",
39-
"test:d1:core": "tsx --test --test-concurrency=5 scripts/orbit-site-authorization-tests.ts scripts/orbit-site-signin-e2e-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts",
38+
"test:d1": "tsx --test --test-concurrency=5 scripts/orbit-test-ports-tests.ts scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-site-authorization-tests.ts scripts/orbit-site-signin-e2e-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-single-renderer-tests.ts scripts/orbit-style-scale-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts scripts/orbit-slice4-tests.ts scripts/orbit-slice5-tests.ts",
39+
"test:d1:core": "tsx --test --test-concurrency=5 scripts/orbit-test-ports-tests.ts scripts/orbit-site-authorization-tests.ts scripts/orbit-site-signin-e2e-tests.ts scripts/orbit-mcp-authorization-tests.ts scripts/orbit-agent-identity-tests.ts scripts/orbit-api-contract-tests.ts scripts/orbit-handle-policy-tests.ts scripts/orbit-d1-tests.ts scripts/orbit-email-tests.ts scripts/orbit-public-page-tests.ts scripts/orbit-public-repository-tests.ts scripts/orbit-slice1-tests.ts scripts/orbit-slice1-worker-tests.ts scripts/orbit-slice3-tests.ts",
4040
"test:d1:publication": "tsx --test scripts/orbit-slice4-tests.ts scripts/orbit-single-renderer-tests.ts scripts/orbit-style-scale-tests.ts",
4141
"test:d1:platform": "tsx --test scripts/orbit-slice5-tests.ts",
4242
"v6:check": "npm run test:d1 && npm run check && npm run worker:build",

‎scripts/orbit-mcp-authorization-tests.ts‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
33
import { mkdtemp, rm } from 'node:fs/promises';
4-
import { createServer } from 'node:net';
54
import { tmpdir } from 'node:os';
65
import path from 'node:path';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import { BACKUP_SCHEMA_VERSION } from '../src/server/backup/dynamic-backup';
99
import {
1010
createOpaqueToken,
@@ -47,23 +47,6 @@ function runMigrations(): string {
4747
return `${result.stdout}\n${result.stderr}`;
4848
}
4949

50-
async function availablePort(): Promise<number> {
51-
return await new Promise((resolve, reject) => {
52-
const server = createServer();
53-
server.once('error', reject);
54-
server.listen(0, '127.0.0.1', () => {
55-
const address = server.address();
56-
if (!address || typeof address === 'string') {
57-
server.close();
58-
reject(new Error('Could not allocate a local test port.'));
59-
return;
60-
}
61-
const { port } = address;
62-
server.close((error) => (error ? reject(error) : resolve(port)));
63-
});
64-
});
65-
}
66-
6750
async function waitForWorker(process: ChildProcessWithoutNullStreams): Promise<void> {
6851
const deadline = Date.now() + 20_000;
6952
let output = '';
@@ -129,9 +112,7 @@ function grantData(prefix: string, accountId: string, agentId: string, now: numb
129112
before(async () => {
130113
persistDirectory = await mkdtemp(path.join(tmpdir(), 'orbit-mcp-auth-d1-'));
131114
migrationOutput = runMigrations();
132-
const port = await availablePort();
133-
let inspectorPort = await availablePort();
134-
while (inspectorPort === port) inspectorPort = await availablePort();
115+
const { port, inspectorPort } = await reserveWorkerPorts();
135116
baseUrl = `http://127.0.0.1:${port}`;
136117
worker = spawn(
137118
process.execPath,

‎scripts/orbit-site-authorization-tests.ts‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
33
import { mkdtemp, rm } from 'node:fs/promises';
4-
import { createServer } from 'node:net';
54
import { tmpdir } from 'node:os';
65
import path from 'node:path';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import {
99
normalizeSiteAuthorizationScopes,
1010
parseSiteAuthorizationScopes,
@@ -75,23 +75,6 @@ function runMigrations(): string {
7575
return `${result.stdout}\n${result.stderr}`;
7676
}
7777

78-
async function availablePort(): Promise<number> {
79-
return await new Promise((resolve, reject) => {
80-
const server = createServer();
81-
server.once('error', reject);
82-
server.listen(0, '127.0.0.1', () => {
83-
const address = server.address();
84-
if (!address || typeof address === 'string') {
85-
server.close();
86-
reject(new Error('Could not allocate a local test port.'));
87-
return;
88-
}
89-
const { port } = address;
90-
server.close((error) => (error ? reject(error) : resolve(port)));
91-
});
92-
});
93-
}
94-
9578
async function waitForWorker(process: ChildProcessWithoutNullStreams): Promise<void> {
9679
const deadline = Date.now() + 20_000;
9780
let output = '';
@@ -263,9 +246,7 @@ async function issueTokens(input: {
263246
before(async () => {
264247
persistDirectory = await mkdtemp(path.join(tmpdir(), 'orbit-site-auth-d1-'));
265248
migrationOutput = runMigrations();
266-
const port = await availablePort();
267-
let inspectorPort = await availablePort();
268-
while (inspectorPort === port) inspectorPort = await availablePort();
249+
const { port, inspectorPort } = await reserveWorkerPorts();
269250
baseUrl = `http://127.0.0.1:${port}`;
270251
worker = spawn(
271252
process.execPath,

‎scripts/orbit-site-signin-e2e-tests.ts‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
*/
1414
import assert from 'node:assert/strict';
1515
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
16-
import { createServer } from 'node:net';
1716
import { mkdtemp, rm } from 'node:fs/promises';
1817
import { tmpdir } from 'node:os';
1918
import path from 'node:path';
2019
import { after, before, describe, test } from 'node:test';
20+
import { reserveWorkerPorts } from './orbit-test-ports';
2121
import {
2222
createOpaqueToken,
2323
hmacDigest,
@@ -62,30 +62,11 @@ function wrangler(args: string[]): void {
6262
if (result.status !== 0) throw new Error(`${result.stdout}\n${result.stderr}`);
6363
}
6464

65-
async function availablePort(): Promise<number> {
66-
return await new Promise((resolve, reject) => {
67-
const server = createServer();
68-
server.once('error', reject);
69-
server.listen(0, '127.0.0.1', () => {
70-
const address = server.address();
71-
if (!address || typeof address === 'string') {
72-
server.close();
73-
reject(new Error('Could not allocate a local test port.'));
74-
return;
75-
}
76-
const { port } = address;
77-
server.close((error) => (error ? reject(error) : resolve(port)));
78-
});
79-
});
80-
}
81-
8265
async function startWorker(persist: string): Promise<{
8366
process: ChildProcessWithoutNullStreams;
8467
url: string;
8568
}> {
86-
const port = await availablePort();
87-
let inspectorPort = await availablePort();
88-
while (inspectorPort === port) inspectorPort = await availablePort();
69+
const { port, inspectorPort } = await reserveWorkerPorts();
8970
let output = '';
9071
const child = spawn(process.execPath, [
9172
WRANGLER, 'dev', '--config', CONFIG, '--local', `--port=${port}`,

‎scripts/orbit-slice1-tests.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
33
import { mkdtemp, rm } from 'node:fs/promises';
4-
import { createServer } from 'node:net';
54
import { tmpdir } from 'node:os';
65
import path from 'node:path';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import {
99
CSRF_COOKIE,
1010
CSRF_HEADER,
@@ -91,18 +91,6 @@ async function accountConsent(accountId: string): Promise<{
9191
return row;
9292
}
9393

94-
async function availablePort(): Promise<number> {
95-
return await new Promise((resolve, reject) => {
96-
const server = createServer();
97-
server.once('error', reject);
98-
server.listen(0, '127.0.0.1', () => {
99-
const address = server.address();
100-
if (!address || typeof address === 'string') return reject(new Error('port_unavailable'));
101-
server.close((error) => error ? reject(error) : resolve(address.port));
102-
});
103-
});
104-
}
105-
10694
async function waitForWorker(process: ChildProcessWithoutNullStreams): Promise<void> {
10795
const deadline = Date.now() + 20_000;
10896
let output = '';
@@ -310,9 +298,7 @@ function authenticatedHeaders(cookies: Map<string, string>, csrf = false): Heade
310298
before(async () => {
311299
persistDirectory = await mkdtemp(path.join(tmpdir(), 'orbit-v6-slice1-'));
312300
migrate();
313-
const port = await availablePort();
314-
let inspectorPort = await availablePort();
315-
while (inspectorPort === port) inspectorPort = await availablePort();
301+
const { port, inspectorPort } = await reserveWorkerPorts();
316302
baseUrl = `http://127.0.0.1:${port}`;
317303
worker = spawn(process.execPath, [
318304
WRANGLER,

‎scripts/orbit-slice3-tests.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
33
import { mkdtemp, rm } from 'node:fs/promises';
4-
import { createServer } from 'node:net';
54
import { tmpdir } from 'node:os';
65
import path from 'node:path';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import { loadManifest, verifyManifest } from './orbit-slice3-manifest';
99

1010
const ROOT = process.cwd();
@@ -46,18 +46,6 @@ function runImporter(): Record<string, number> {
4646
return JSON.parse(result.stdout.trim()) as Record<string, number>;
4747
}
4848

49-
async function availablePort(): Promise<number> {
50-
return await new Promise((resolve, reject) => {
51-
const server = createServer();
52-
server.once('error', reject);
53-
server.listen(0, '127.0.0.1', () => {
54-
const address = server.address();
55-
if (!address || typeof address === 'string') return reject(new Error('port_unavailable'));
56-
server.close((error) => error ? reject(error) : resolve(address.port));
57-
});
58-
});
59-
}
60-
6149
async function waitForWorker(process: ChildProcessWithoutNullStreams): Promise<void> {
6250
const deadline = Date.now() + 20_000;
6351
let output = '';
@@ -88,9 +76,7 @@ before(async () => {
8876
persistDirectory = await mkdtemp(path.join(tmpdir(), 'orbit-v6-slice3-'));
8977
migrate();
9078
runImporter();
91-
const port = await availablePort();
92-
let inspectorPort = await availablePort();
93-
while (inspectorPort === port) inspectorPort = await availablePort();
79+
const { port, inspectorPort } = await reserveWorkerPorts();
9480
baseUrl = `http://127.0.0.1:${port}`;
9581
worker = spawn(process.execPath, [
9682
WRANGLER, 'dev', '--config', CONFIG, '--local', `--port=${port}`, `--inspector-port=${inspectorPort}`,

‎scripts/orbit-slice4-tests.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
3-
import { createServer } from 'node:net';
43
import path from 'node:path';
54
import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises';
65
import { tmpdir } from 'node:os';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import { createEntityId } from '../src/server/foundation/ids';
99
import { createOpaqueToken, hmacDigest, randomBase64Url, sha256Base64Url } from '../src/server/identity/tokens';
1010
import { canonicalJson } from '../src/server/publication/content';
@@ -70,22 +70,8 @@ function importLegacy(persist: string): void {
7070
if (result.status !== 0) throw new Error(`${result.stdout}\n${result.stderr}`);
7171
}
7272

73-
async function availablePort(): Promise<number> {
74-
return await new Promise((resolve, reject) => {
75-
const server = createServer();
76-
server.once('error', reject);
77-
server.listen(0, '127.0.0.1', () => {
78-
const address = server.address();
79-
if (!address || typeof address === 'string') return reject(new Error('port_unavailable'));
80-
server.close((error) => error ? reject(error) : resolve(address.port));
81-
});
82-
});
83-
}
84-
8573
async function startWorker(persist: string): Promise<{ process: ChildProcessWithoutNullStreams; url: string; output: () => string }> {
86-
const port = await availablePort();
87-
let inspectorPort = await availablePort();
88-
while (inspectorPort === port) inspectorPort = await availablePort();
74+
const { port, inspectorPort } = await reserveWorkerPorts();
8975
const url = `http://127.0.0.1:${port}`;
9076
let output = '';
9177
const child = spawn(process.execPath, [

‎scripts/orbit-slice5-tests.ts‎

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import assert from 'node:assert/strict';
22
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from 'node:child_process';
3-
import { createServer } from 'node:net';
43
import path from 'node:path';
54
import { mkdtemp, rm } from 'node:fs/promises';
65
import { tmpdir } from 'node:os';
76
import { after, before, describe, test } from 'node:test';
7+
import { reserveWorkerPorts } from './orbit-test-ports';
88
import { createEntityId } from '../src/server/foundation/ids';
99
import { createOpaqueToken, hmacDigest, randomBase64Url, sha256Base64Url } from '../src/server/identity/tokens';
1010
import { canonicalJson } from '../src/server/publication/content';
@@ -59,22 +59,8 @@ function importLegacy(persist: string): void {
5959
if (result.status !== 0) throw new Error(`${result.stdout}\n${result.stderr}`);
6060
}
6161

62-
async function availablePort(): Promise<number> {
63-
return await new Promise((resolve, reject) => {
64-
const server = createServer();
65-
server.once('error', reject);
66-
server.listen(0, '127.0.0.1', () => {
67-
const address = server.address();
68-
if (!address || typeof address === 'string') return reject(new Error('port_unavailable'));
69-
server.close((error) => error ? reject(error) : resolve(address.port));
70-
});
71-
});
72-
}
73-
7462
async function startWorker(persist: string) {
75-
const port = await availablePort();
76-
let inspectorPort = await availablePort();
77-
while (inspectorPort === port) inspectorPort = await availablePort();
63+
const { port, inspectorPort } = await reserveWorkerPorts();
7864
let output = '';
7965
const child = spawn(process.execPath, [
8066
WRANGLER, 'dev', '--config', CONFIG, '--local', `--port=${port}`, `--inspector-port=${inspectorPort}`, `--persist-to=${persist}`,

0 commit comments

Comments
 (0)