@sproutboat/runtime 0.41.0 → 0.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +287 -2
- package/src/sockets.test.ts +42 -0
- package/src/source.ts +15 -0
- package/src/transport-broker.js +471 -0
- package/src/transport-embedded.js +175 -1
- package/src/wrap.ts +14 -0
- package/test/native-sockets-broker.ts +460 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
|
+
import tls from "node:tls";
|
|
7
|
+
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
8
|
+
import { createBroker, listen } from "../../wire/src/broker";
|
|
9
|
+
import { EMPTY_BINDINGS, preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
10
|
+
|
|
11
|
+
// cloudflare:sockets connect() on the broker transport (compatibility date 2026-11-17): a
|
|
12
|
+
// sprout built with the broker transport opens a second loopback connection to the broker per
|
|
13
|
+
// socket, and the broker vets the address, connects, and terminates TLS. Local servers play the
|
|
14
|
+
// origins: an echo, a server that answers once it sees the end of input, a STARTTLS server and a
|
|
15
|
+
// TLS echo. The handler is what a driver does; measurements are printed at the end.
|
|
16
|
+
const handler = `
|
|
17
|
+
import { connect } from "cloudflare:sockets";
|
|
18
|
+
const enc = new TextEncoder();
|
|
19
|
+
const dec = new TextDecoder();
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
const withTimeout = (p, ms, what) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("TIMEOUT " + what)), ms))]);
|
|
22
|
+
async function readN(reader, n) {
|
|
23
|
+
let out = "";
|
|
24
|
+
while (out.length < n) {
|
|
25
|
+
const r = await reader.read();
|
|
26
|
+
if (r.done) return out;
|
|
27
|
+
out += dec.decode(r.value);
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
async function readAll(reader) {
|
|
32
|
+
let out = "";
|
|
33
|
+
for (;;) {
|
|
34
|
+
const r = await reader.read();
|
|
35
|
+
if (r.done) return out;
|
|
36
|
+
out += dec.decode(r.value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const held = [];
|
|
40
|
+
export default {
|
|
41
|
+
async fetch(request) {
|
|
42
|
+
const url = new URL(request.url);
|
|
43
|
+
const q = url.searchParams;
|
|
44
|
+
const port = Number(q.get("port"));
|
|
45
|
+
try {
|
|
46
|
+
if (url.pathname === "/ping") return new Response("pong");
|
|
47
|
+
if (url.pathname === "/echo") {
|
|
48
|
+
const s = connect("127.0.0.1:" + port);
|
|
49
|
+
const w = s.writable.getWriter();
|
|
50
|
+
const r = s.readable.getReader();
|
|
51
|
+
const info = await withTimeout(s.opened, 5000, "opened");
|
|
52
|
+
await w.write(enc.encode("hello\\n"));
|
|
53
|
+
const got = await withTimeout(readN(r, 6), 5000, "echo");
|
|
54
|
+
await s.close();
|
|
55
|
+
return Response.json({ got, remote: typeof info.remoteAddress });
|
|
56
|
+
}
|
|
57
|
+
if (url.pathname === "/halfclose") {
|
|
58
|
+
const s = connect({ hostname: "127.0.0.1", port }, { allowHalfOpen: true });
|
|
59
|
+
const w = s.writable.getWriter();
|
|
60
|
+
const r = s.readable.getReader();
|
|
61
|
+
await w.write(enc.encode("one two"));
|
|
62
|
+
await w.close();
|
|
63
|
+
const got = await withTimeout(readAll(r), 5000, "halfclose");
|
|
64
|
+
await s.close().catch(() => {});
|
|
65
|
+
return Response.json({ got });
|
|
66
|
+
}
|
|
67
|
+
if (url.pathname === "/starttls") {
|
|
68
|
+
const s = connect("127.0.0.1:" + port, { secureTransport: "starttls" });
|
|
69
|
+
const w = s.writable.getWriter();
|
|
70
|
+
const r = s.readable.getReader();
|
|
71
|
+
const greeting = await withTimeout(readN(r, 11), 5000, "greeting");
|
|
72
|
+
await w.write(enc.encode("STARTTLS\\r\\n"));
|
|
73
|
+
const go = await withTimeout(readN(r, 4), 5000, "go");
|
|
74
|
+
r.releaseLock();
|
|
75
|
+
w.releaseLock();
|
|
76
|
+
const secure = s.startTls({ expectedServerHostname: "localhost" });
|
|
77
|
+
const w2 = secure.writable.getWriter();
|
|
78
|
+
const r2 = secure.readable.getReader();
|
|
79
|
+
await withTimeout(secure.opened, 5000, "tls");
|
|
80
|
+
await w2.write(enc.encode("ping"));
|
|
81
|
+
const echoed = await withTimeout(readN(r2, 8), 5000, "tls echo");
|
|
82
|
+
await secure.close();
|
|
83
|
+
return Response.json({ greeting, go, echoed, upgraded: s.upgraded });
|
|
84
|
+
}
|
|
85
|
+
if (url.pathname === "/tls") {
|
|
86
|
+
const host = q.get("host") || "localhost";
|
|
87
|
+
const s = connect({ hostname: host, port }, { secureTransport: "on" });
|
|
88
|
+
const w = s.writable.getWriter();
|
|
89
|
+
const r = s.readable.getReader();
|
|
90
|
+
await withTimeout(s.opened, 5000, "tls opened");
|
|
91
|
+
await w.write(enc.encode("secret"));
|
|
92
|
+
const got = await withTimeout(readN(r, 6), 5000, "tls echo");
|
|
93
|
+
await s.close();
|
|
94
|
+
return Response.json({ got });
|
|
95
|
+
}
|
|
96
|
+
if (url.pathname === "/refused") {
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const target of ["169.254.169.254:80", "10.1.2.3:80", "127.0.0.1:25"]) {
|
|
99
|
+
const s = connect(target);
|
|
100
|
+
try {
|
|
101
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
102
|
+
out[target] = "opened";
|
|
103
|
+
} catch (e) {
|
|
104
|
+
out[target] = e.name + ": " + e.message;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Response.json(out);
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname === "/untrusted") {
|
|
110
|
+
const s = connect({ hostname: "localhost", port }, { secureTransport: "on" });
|
|
111
|
+
try {
|
|
112
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
113
|
+
return Response.json({ opened: true });
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return Response.json({ opened: false, name: e.name });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (url.pathname === "/bulk") {
|
|
119
|
+
const N = 1 << 20;
|
|
120
|
+
const s = connect("127.0.0.1:" + port);
|
|
121
|
+
const w = s.writable.getWriter();
|
|
122
|
+
const rd = s.readable.getReader();
|
|
123
|
+
const data = new Uint8Array(N);
|
|
124
|
+
for (let i = 0; i < N; i++) data[i] = (i * 31 + 7) & 255;
|
|
125
|
+
let sum = 0;
|
|
126
|
+
let got = 0;
|
|
127
|
+
const reading = (async () => {
|
|
128
|
+
while (got < N) {
|
|
129
|
+
const r = await rd.read();
|
|
130
|
+
if (r.done) break;
|
|
131
|
+
for (let i = 0; i < r.value.length; i++) sum = (sum + r.value[i]) % 1000003;
|
|
132
|
+
got += r.value.length;
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
135
|
+
for (let o = 0; o < N; o += 65536) await w.write(data.subarray(o, o + 65536));
|
|
136
|
+
await withTimeout(reading, 20000, "bulk");
|
|
137
|
+
let want = 0;
|
|
138
|
+
for (let i = 0; i < N; i++) want = (want + data[i]) % 1000003;
|
|
139
|
+
await s.close();
|
|
140
|
+
return Response.json({ got, checksumOk: sum === want });
|
|
141
|
+
}
|
|
142
|
+
if (url.pathname === "/latency") {
|
|
143
|
+
const t0 = Date.now();
|
|
144
|
+
const s = connect("127.0.0.1:" + port);
|
|
145
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
146
|
+
const ms = Date.now() - t0;
|
|
147
|
+
await s.close();
|
|
148
|
+
return Response.json({ ms });
|
|
149
|
+
}
|
|
150
|
+
if (url.pathname === "/latency-many") {
|
|
151
|
+
const n = Number(q.get("n"));
|
|
152
|
+
const times = [];
|
|
153
|
+
for (let i = 0; i < n; i++) {
|
|
154
|
+
const t0 = performance.now();
|
|
155
|
+
const s = connect("127.0.0.1:" + port);
|
|
156
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
157
|
+
times.push(performance.now() - t0);
|
|
158
|
+
await s.close();
|
|
159
|
+
}
|
|
160
|
+
times.sort((a, b) => a - b);
|
|
161
|
+
return Response.json({ n, median: times[Math.floor(n / 2)], p95: times[Math.floor(n * 0.95)], max: times[n - 1] });
|
|
162
|
+
}
|
|
163
|
+
if (url.pathname === "/rtt") {
|
|
164
|
+
const n = Number(q.get("n"));
|
|
165
|
+
const s = connect("127.0.0.1:" + port);
|
|
166
|
+
const w = s.writable.getWriter();
|
|
167
|
+
const r = s.readable.getReader();
|
|
168
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
169
|
+
const msg = enc.encode("0123456789abcdef");
|
|
170
|
+
const t0 = performance.now();
|
|
171
|
+
for (let i = 0; i < n; i++) {
|
|
172
|
+
await w.write(msg);
|
|
173
|
+
await withTimeout(readN(r, 16), 5000, "rtt");
|
|
174
|
+
}
|
|
175
|
+
const ms = performance.now() - t0;
|
|
176
|
+
await s.close();
|
|
177
|
+
return Response.json({ n, ms });
|
|
178
|
+
}
|
|
179
|
+
if (url.pathname === "/hold") {
|
|
180
|
+
const k = Number(q.get("k"));
|
|
181
|
+
for (const s of held.splice(0)) await s.close();
|
|
182
|
+
for (let i = 0; i < k; i++) {
|
|
183
|
+
const s = connect("127.0.0.1:" + port);
|
|
184
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
185
|
+
held.push(s);
|
|
186
|
+
}
|
|
187
|
+
return Response.json({ held: held.length });
|
|
188
|
+
}
|
|
189
|
+
if (url.pathname === "/release") {
|
|
190
|
+
for (const s of held.splice(0)) await s.close();
|
|
191
|
+
return Response.json({ held: held.length });
|
|
192
|
+
}
|
|
193
|
+
if (url.pathname === "/seventeen") {
|
|
194
|
+
const out = [];
|
|
195
|
+
for (let i = 0; i < 17; i++) {
|
|
196
|
+
const s = connect("127.0.0.1:" + port);
|
|
197
|
+
held.push(s);
|
|
198
|
+
try {
|
|
199
|
+
await withTimeout(s.opened, 5000, "opened");
|
|
200
|
+
out.push("ok");
|
|
201
|
+
} catch (e) {
|
|
202
|
+
out.push(e.message);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
for (const s of held.splice(0)) await s.close();
|
|
206
|
+
return Response.json({ ok: out.filter((o) => o === "ok").length, last: out[16] });
|
|
207
|
+
}
|
|
208
|
+
return new Response("no route", { status: 404 });
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return Response.json({ error: String(e && e.message) }, { status: 500 });
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
function freePort(): number {
|
|
217
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
218
|
+
const port = listener.port;
|
|
219
|
+
listener.stop(true);
|
|
220
|
+
return port;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-sockets-broker-"));
|
|
224
|
+
const servers: Array<{ close(): void }> = [];
|
|
225
|
+
|
|
226
|
+
// A self-signed certificate for localhost; the broker is told to trust it (tests only).
|
|
227
|
+
const made = Bun.spawnSync([
|
|
228
|
+
"openssl",
|
|
229
|
+
"req",
|
|
230
|
+
"-x509",
|
|
231
|
+
"-newkey",
|
|
232
|
+
"rsa:2048",
|
|
233
|
+
"-nodes",
|
|
234
|
+
"-keyout",
|
|
235
|
+
join(workdir, "key.pem"),
|
|
236
|
+
"-out",
|
|
237
|
+
join(workdir, "cert.pem"),
|
|
238
|
+
"-days",
|
|
239
|
+
"2",
|
|
240
|
+
"-subj",
|
|
241
|
+
"/CN=localhost",
|
|
242
|
+
"-addext",
|
|
243
|
+
"subjectAltName=DNS:localhost",
|
|
244
|
+
]);
|
|
245
|
+
assert.equal(made.exitCode, 0, `openssl failed: ${made.stderr}`);
|
|
246
|
+
const cert = await readFile(join(workdir, "cert.pem"), "utf8");
|
|
247
|
+
const key = await readFile(join(workdir, "key.pem"), "utf8");
|
|
248
|
+
|
|
249
|
+
const listenOn = (server: net.Server): number => {
|
|
250
|
+
server.listen(0, "127.0.0.1");
|
|
251
|
+
servers.push(server);
|
|
252
|
+
// SAFETY: a listening TCP server's address() is an AddressInfo.
|
|
253
|
+
return (server.address() as net.AddressInfo).port;
|
|
254
|
+
};
|
|
255
|
+
const echoPort = listenOn(net.createServer((s) => s.pipe(s)));
|
|
256
|
+
const answersAtEnd = listenOn(
|
|
257
|
+
net.createServer({ allowHalfOpen: true }, (s) => {
|
|
258
|
+
const parts: Buffer[] = [];
|
|
259
|
+
s.on("data", (c) => parts.push(c));
|
|
260
|
+
s.on("end", () => s.end(`got ${Buffer.concat(parts).toString()}`));
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
const starttlsPort = listenOn(
|
|
264
|
+
net.createServer((s) => {
|
|
265
|
+
s.write("220 hello\r\n");
|
|
266
|
+
s.once("data", () => {
|
|
267
|
+
s.write("go\r\n");
|
|
268
|
+
const secure = new tls.TLSSocket(s, { isServer: true, key, cert });
|
|
269
|
+
secure.on("data", (c) => secure.write(Buffer.concat([Buffer.from("tls:"), c])));
|
|
270
|
+
secure.on("error", () => {});
|
|
271
|
+
});
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
const tlsPort = listenOn(
|
|
275
|
+
tls.createServer({ key, cert }, (s) => {
|
|
276
|
+
s.on("data", (c) => s.write(c));
|
|
277
|
+
s.on("error", () => {});
|
|
278
|
+
}),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const broker = createBroker({
|
|
282
|
+
db: ":memory:",
|
|
283
|
+
token: "native-sockets",
|
|
284
|
+
bindings: { ...EMPTY_BINDINGS, resources: {} },
|
|
285
|
+
egressAllow: ["127.0.0.1"],
|
|
286
|
+
tcp: { tls: { ca: cert } },
|
|
287
|
+
});
|
|
288
|
+
const brokerServer = listen(broker, "127.0.0.1", 0);
|
|
289
|
+
const untrustingBroker = createBroker({ db: ":memory:", token: "native-sockets", egressAllow: ["127.0.0.1"] });
|
|
290
|
+
const untrustingServer = listen(untrustingBroker, "127.0.0.1", 0);
|
|
291
|
+
|
|
292
|
+
let sprout: ReturnType<typeof Bun.spawn> | undefined;
|
|
293
|
+
let untrusting: ReturnType<typeof Bun.spawn> | undefined;
|
|
294
|
+
const port = freePort();
|
|
295
|
+
const port2 = freePort();
|
|
296
|
+
const rssKiB = (pid: number) =>
|
|
297
|
+
Number(
|
|
298
|
+
Bun.spawnSync(["ps", "-o", "rss=", "-p", String(pid)])
|
|
299
|
+
.stdout.toString()
|
|
300
|
+
.trim(),
|
|
301
|
+
);
|
|
302
|
+
const get = async (path: string, base = port) => {
|
|
303
|
+
const res = await fetch(`http://127.0.0.1:${base}${path}`, { signal: AbortSignal.timeout(60_000) });
|
|
304
|
+
const text = await res.text();
|
|
305
|
+
try {
|
|
306
|
+
return JSON.parse(text);
|
|
307
|
+
} catch {
|
|
308
|
+
return text;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
try {
|
|
313
|
+
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
314
|
+
const node = Bun.which("node");
|
|
315
|
+
if (!node) throw new Error("native sockets test needs node on PATH");
|
|
316
|
+
const [core, transport] = await Promise.all([
|
|
317
|
+
readFile(preludePath, "utf8"),
|
|
318
|
+
readFile(transportPath("broker"), "utf8"),
|
|
319
|
+
]);
|
|
320
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
321
|
+
const generated = join(workdir, "sockets.generated.js");
|
|
322
|
+
const binary = join(workdir, "sockets-native");
|
|
323
|
+
await writeFile(
|
|
324
|
+
generated,
|
|
325
|
+
wrapNativeFetchHandler(
|
|
326
|
+
handler,
|
|
327
|
+
core.replace(TRANSPORT_MARKER, transport),
|
|
328
|
+
{},
|
|
329
|
+
EMPTY_BINDINGS,
|
|
330
|
+
undefined,
|
|
331
|
+
"2026-11-17",
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
const compile = Bun.spawn(
|
|
335
|
+
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
336
|
+
{
|
|
337
|
+
cwd: workdir,
|
|
338
|
+
env: process.env,
|
|
339
|
+
stdout: "pipe",
|
|
340
|
+
stderr: "pipe",
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
344
|
+
compile.exited,
|
|
345
|
+
new Response(compile.stdout).text(),
|
|
346
|
+
new Response(compile.stderr).text(),
|
|
347
|
+
]);
|
|
348
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
349
|
+
|
|
350
|
+
const start = async (listening: number, brokerPort: number) => {
|
|
351
|
+
const child = Bun.spawn([binary], {
|
|
352
|
+
cwd: workdir,
|
|
353
|
+
env: {
|
|
354
|
+
...process.env,
|
|
355
|
+
PORT: String(listening),
|
|
356
|
+
SB_BROKER_PORT: String(brokerPort),
|
|
357
|
+
SB_BROKER_TOKEN: "native-sockets",
|
|
358
|
+
},
|
|
359
|
+
stdout: "ignore",
|
|
360
|
+
stderr: "inherit",
|
|
361
|
+
});
|
|
362
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
363
|
+
try {
|
|
364
|
+
if ((await get("/ping", listening)) === "pong") return child;
|
|
365
|
+
} catch {
|
|
366
|
+
if (child.exitCode !== null) break;
|
|
367
|
+
await Bun.sleep(100);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
throw new Error("compiled handler did not start");
|
|
371
|
+
};
|
|
372
|
+
sprout = await start(port, brokerServer.port);
|
|
373
|
+
untrusting = await start(port2, untrustingServer.port);
|
|
374
|
+
|
|
375
|
+
assert.deepEqual(await get(`/echo?port=${echoPort}`), { got: "hello\n", remote: "string" });
|
|
376
|
+
console.log("echo through the broker tunnel");
|
|
377
|
+
|
|
378
|
+
assert.deepEqual(await get(`/halfclose?port=${answersAtEnd}`), { got: "got one two" });
|
|
379
|
+
console.log("half-close: the writer closes, the reply still arrives, then the end of input");
|
|
380
|
+
|
|
381
|
+
assert.deepEqual(await get(`/starttls?port=${starttlsPort}`), {
|
|
382
|
+
greeting: "220 hello\r\n",
|
|
383
|
+
go: "go\r\n",
|
|
384
|
+
echoed: "tls:ping",
|
|
385
|
+
upgraded: false,
|
|
386
|
+
});
|
|
387
|
+
console.log("startTls upgrades in the broker, verified against the expected host name");
|
|
388
|
+
|
|
389
|
+
assert.deepEqual(await get(`/tls?port=${tlsPort}`), { got: "secret" });
|
|
390
|
+
const wrongName = await get(`/tls?port=${tlsPort}&host=127.0.0.1`);
|
|
391
|
+
assert.match(String(wrongName.error), /proxy request failed, cannot connect/);
|
|
392
|
+
console.log("secureTransport on: TLS ends in the broker; a name the certificate lacks fails");
|
|
393
|
+
|
|
394
|
+
assert.deepEqual(await get(`/untrusted?port=${tlsPort}`, port2), { opened: false, name: "Error" });
|
|
395
|
+
console.log("an untrusted certificate is refused");
|
|
396
|
+
|
|
397
|
+
const refused = await get("/refused");
|
|
398
|
+
for (const target of ["169.254.169.254:80", "10.1.2.3:80", "127.0.0.1:25"]) {
|
|
399
|
+
assert.match(refused[target], /proxy request failed, cannot connect to the specified address/, target);
|
|
400
|
+
}
|
|
401
|
+
console.log("private addresses and port 25 are refused like fetch");
|
|
402
|
+
|
|
403
|
+
assert.deepEqual(await get(`/bulk?port=${echoPort}`), { got: 1 << 20, checksumOk: true });
|
|
404
|
+
console.log("1 MiB echoed through the tunnel with its checksum intact");
|
|
405
|
+
|
|
406
|
+
const seventeen = await get(`/seventeen?port=${echoPort}`);
|
|
407
|
+
assert.equal(seventeen.ok, 16);
|
|
408
|
+
assert.match(seventeen.last, /Too many open connections/);
|
|
409
|
+
for (let i = 0; i < 100 && broker.tcp.open() > 0; i++) await Bun.sleep(20);
|
|
410
|
+
assert.equal(broker.tcp.open(), 0, "closing the sockets frees the broker's slots");
|
|
411
|
+
console.log("the sixteenth socket opens, the seventeenth is refused, closing frees the slots");
|
|
412
|
+
|
|
413
|
+
// Cleanup on sprout exit: held sockets are torn down when the sprout dies.
|
|
414
|
+
assert.deepEqual(await get(`/hold?port=${echoPort}&k=3`), { held: 3 });
|
|
415
|
+
assert.equal(broker.tcp.open(), 3);
|
|
416
|
+
sprout.kill(9);
|
|
417
|
+
await sprout.exited;
|
|
418
|
+
sprout = undefined;
|
|
419
|
+
for (let i = 0; i < 50 && broker.tcp.open() > 0; i++) await Bun.sleep(20);
|
|
420
|
+
assert.equal(broker.tcp.open(), 0, "the broker cleans up when the sprout disconnects");
|
|
421
|
+
console.log("a dead sprout's sockets are cleaned up in the broker");
|
|
422
|
+
|
|
423
|
+
// --- measurements
|
|
424
|
+
sprout = await start(port, brokerServer.port);
|
|
425
|
+
const latency = await get(`/latency-many?port=${echoPort}&n=200`);
|
|
426
|
+
const rtt = await get(`/rtt?port=${echoPort}&n=2000`);
|
|
427
|
+
await get(`/hold?port=${echoPort}&k=1`);
|
|
428
|
+
await get(`/release?`);
|
|
429
|
+
const rss = new Map<string, number>([["base", rssKiB(sprout.pid)]]);
|
|
430
|
+
for (const k of [1, 8, 16]) {
|
|
431
|
+
await get(`/release?`);
|
|
432
|
+
await Bun.sleep(300);
|
|
433
|
+
const before = rssKiB(sprout.pid);
|
|
434
|
+
await get(`/hold?port=${echoPort}&k=${k}`);
|
|
435
|
+
await Bun.sleep(300);
|
|
436
|
+
const after = rssKiB(sprout.pid);
|
|
437
|
+
rss.set(`open${k}`, after);
|
|
438
|
+
rss.set(`perSocketKiB${k}`, Math.round(((after - before) / k) * 10) / 10);
|
|
439
|
+
}
|
|
440
|
+
console.log(
|
|
441
|
+
JSON.stringify({
|
|
442
|
+
connectLatencyMs: latency,
|
|
443
|
+
roundTrips: { ...rtt, perTripMs: rtt.ms / rtt.n },
|
|
444
|
+
rssKiB: Object.fromEntries(rss),
|
|
445
|
+
}),
|
|
446
|
+
);
|
|
447
|
+
} finally {
|
|
448
|
+
sprout?.kill(9);
|
|
449
|
+
untrusting?.kill(9);
|
|
450
|
+
await sprout?.exited;
|
|
451
|
+
await untrusting?.exited;
|
|
452
|
+
brokerServer.stop();
|
|
453
|
+
untrustingServer.stop();
|
|
454
|
+
broker.close();
|
|
455
|
+
untrustingBroker.close();
|
|
456
|
+
for (const server of servers) server.close();
|
|
457
|
+
await rm(workdir, { recursive: true, force: true });
|
|
458
|
+
}
|
|
459
|
+
// Origin sockets still open would keep the process alive.
|
|
460
|
+
process.exit(0);
|