Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 7d0ed16

Browse files
committed
Reuse allocated port/inspectorPort: 0 ports between setOptions()
1 parent a915982 commit 7d0ed16

File tree

3 files changed

+87
-29
lines changed

3 files changed

+87
-29
lines changed

packages/miniflare/src/index.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ import {
8181
Service,
8282
Socket,
8383
SocketIdentifier,
84+
SocketPorts,
8485
Worker_Binding,
8586
Worker_Module,
8687
kInspectorSocket,
@@ -546,14 +547,15 @@ export function _initialiseInstanceRegistry() {
546547
}
547548

548549
export class Miniflare {
550+
#previousSharedOpts?: PluginSharedOptions;
549551
#sharedOpts: PluginSharedOptions;
550552
#workerOpts: PluginWorkerOptions[];
551553
#log: Log;
552554

553555
readonly #runtime?: Runtime;
554556
readonly #removeRuntimeExitHook?: () => void;
555557
#runtimeEntryURL?: URL;
556-
#socketPorts?: Map<SocketIdentifier, number>;
558+
#socketPorts?: SocketPorts;
557559
#runtimeClient?: Client;
558560
#proxyClient?: ProxyClient;
559561

@@ -877,6 +879,21 @@ export class Miniflare {
877879
});
878880
}
879881

882+
#getSocketAddress(
883+
id: SocketIdentifier,
884+
previousRequestedPort: number | undefined,
885+
host = DEFAULT_HOST,
886+
requestedPort?: number
887+
) {
888+
// If `port` is set to `0`, was previously set to `0`, and we previously had
889+
// a port for this socket, reuse that random port
890+
if (requestedPort === 0 && previousRequestedPort === 0) {
891+
requestedPort = this.#socketPorts?.get(id);
892+
}
893+
// Otherwise, default to a new random port
894+
return `${host}:${requestedPort ?? 0}`;
895+
}
896+
880897
async #assembleConfig(loopbackPort: number): Promise<Config> {
881898
const allWorkerOpts = this.#workerOpts;
882899
const sharedOpts = this.#sharedOpts;
@@ -967,13 +984,24 @@ export class Miniflare {
967984

968985
// Allow additional sockets to be opened directly to specific workers,
969986
// bypassing Miniflare's entry worker.
970-
let { unsafeDirectHost, unsafeDirectPort } = workerOpts.core;
987+
const { unsafeDirectHost, unsafeDirectPort } = workerOpts.core;
971988
if (unsafeDirectHost !== undefined || unsafeDirectPort !== undefined) {
972-
unsafeDirectHost ??= DEFAULT_HOST;
973-
unsafeDirectPort ??= 0;
989+
const name = getDirectSocketName(i);
990+
const address = this.#getSocketAddress(
991+
name,
992+
// We don't attempt to reuse allocated ports for `unsafeDirectPort: 0`
993+
// as there's not always a clear mapping between current/previous
994+
// worker options. We could do it by index, names, script, etc.
995+
// This is an unsafe option primarily intended for Wrangler's
996+
// inspector proxy, which will usually set this value to `9229`.
997+
// We could consider changing this in the future.
998+
/* previousRequestedPort */ undefined,
999+
unsafeDirectHost,
1000+
unsafeDirectPort
1001+
);
9741002
sockets.push({
975-
name: getDirectSocketName(i),
976-
address: `${unsafeDirectHost}:${unsafeDirectPort}`,
1003+
name,
1004+
address,
9771005
service: { name: getUserServiceName(workerName) },
9781006
http: {},
9791007
});
@@ -1059,13 +1087,27 @@ export class Miniflare {
10591087
const host = this.#sharedOpts.core.host ?? DEFAULT_HOST;
10601088
const urlSafeHost = getURLSafeHost(host);
10611089
const accessibleHost = getAccessibleHost(host);
1090+
const entryAddress = this.#getSocketAddress(
1091+
SOCKET_ENTRY,
1092+
this.#previousSharedOpts?.core.port,
1093+
host,
1094+
this.#sharedOpts.core.port
1095+
);
1096+
let inspectorAddress: string | undefined;
1097+
if (this.#sharedOpts.core.inspectorPort !== undefined) {
1098+
inspectorAddress = this.#getSocketAddress(
1099+
kInspectorSocket,
1100+
this.#previousSharedOpts?.core.inspectorPort,
1101+
"localhost",
1102+
this.#sharedOpts.core.inspectorPort
1103+
);
1104+
}
10621105
const runtimeOpts: Abortable & RuntimeOptions = {
10631106
signal: this.#disposeController.signal,
1064-
entryHost: urlSafeHost,
1065-
entryPort: this.#sharedOpts.core.port ?? 0,
1107+
entryAddress,
10661108
loopbackPort,
10671109
requiredSockets,
1068-
inspectorPort: this.#sharedOpts.core.inspectorPort,
1110+
inspectorAddress,
10691111
verbose: this.#sharedOpts.core.verbose,
10701112
};
10711113
const maybeSocketPorts = await this.#runtime.updateConfig(
@@ -1223,6 +1265,7 @@ export class Miniflare {
12231265

12241266
// Split and validate options
12251267
const [sharedOpts, workerOpts] = validateOptions(opts);
1268+
this.#previousSharedOpts = this.#sharedOpts;
12261269
this.#sharedOpts = sharedOpts;
12271270
this.#workerOpts = workerOpts;
12281271
this.#log = this.#sharedOpts.core.log ?? this.#log;

packages/miniflare/src/runtime/index.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,20 @@ const ControlMessageSchema = z.discriminatedUnion("event", [
2525

2626
export const kInspectorSocket = Symbol("kInspectorSocket");
2727
export type SocketIdentifier = string | typeof kInspectorSocket;
28+
export type SocketPorts = Map<SocketIdentifier, number /* port */>;
2829

2930
export interface RuntimeOptions {
30-
entryHost: string;
31-
entryPort: number;
31+
entryAddress: string;
3232
loopbackPort: number;
3333
requiredSockets: SocketIdentifier[];
34-
inspectorPort?: number;
34+
inspectorAddress?: string;
3535
verbose?: boolean;
3636
}
3737

3838
async function waitForPorts(
3939
stream: Readable,
4040
options: Abortable & Pick<RuntimeOptions, "requiredSockets">
41-
): Promise<Map<SocketIdentifier, number> | undefined> {
41+
): Promise<SocketPorts | undefined> {
4242
if (options?.signal?.aborted) return;
4343
const lines = rl.createInterface(stream);
4444
// Calling `close()` will end the async iterator below and return undefined
@@ -102,16 +102,16 @@ function getRuntimeArgs(options: RuntimeOptions) {
102102
// Required to use compatibility flags without a default-on date,
103103
// (e.g. "streams_enable_constructors"), see https://github.com/cloudflare/workerd/pull/21
104104
"--experimental",
105-
`--socket-addr=${SOCKET_ENTRY}=${options.entryHost}:${options.entryPort}`,
105+
`--socket-addr=${SOCKET_ENTRY}=${options.entryAddress}`,
106106
`--external-addr=${SERVICE_LOOPBACK}=localhost:${options.loopbackPort}`,
107107
// Configure extra pipe for receiving control messages (e.g. when ready)
108108
"--control-fd=3",
109109
// Read config from stdin
110110
"-",
111111
];
112-
if (options.inspectorPort !== undefined) {
112+
if (options.inspectorAddress !== undefined) {
113113
// Required to enable the V8 inspector
114-
args.push(`--inspector-addr=localhost:${options.inspectorPort}`);
114+
args.push(`--inspector-addr=${options.inspectorAddress}`);
115115
}
116116
if (options.verbose) {
117117
args.push("--verbose");
@@ -127,7 +127,7 @@ export class Runtime {
127127
async updateConfig(
128128
configBuffer: Buffer,
129129
options: Abortable & RuntimeOptions
130-
): Promise<Map<SocketIdentifier, number /* port */> | undefined> {
130+
): Promise<SocketPorts | undefined> {
131131
// 1. Stop existing process (if any) and wait for exit
132132
await this.dispose();
133133
// TODO: what happens if runtime crashes?

packages/miniflare/test/index.spec.ts

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ test("Miniflare: setOptions: can update host/port", async (t) => {
130130

131131
const opts: MiniflareOptions = {
132132
port: 0,
133+
inspectorPort: 0,
133134
liveReload: true,
134135
script: `addEventListener("fetch", (event) => {
135136
event.respondWith(new Response("<p>👋</p>", {
@@ -140,24 +141,38 @@ test("Miniflare: setOptions: can update host/port", async (t) => {
140141
const mf = new Miniflare(opts);
141142
t.teardown(() => mf.dispose());
142143

143-
const initialURL = await mf.ready;
144-
let res = await mf.dispatchFetch("http://localhost");
145-
const initialLoopbackPort = loopbackPortRegexp.exec(await res.text())?.[1];
144+
async function getState() {
145+
const url = await mf.ready;
146+
const inspectorUrl = await mf.getInspectorURL();
147+
const res = await mf.dispatchFetch("http://localhost");
148+
const loopbackPort = loopbackPortRegexp.exec(await res.text())?.[1];
149+
return { url, inspectorUrl, loopbackPort };
150+
}
146151

152+
const state1 = await getState();
147153
opts.host = "0.0.0.0";
148154
await mf.setOptions(opts);
149-
const updatedURL = await mf.ready;
150-
res = await mf.dispatchFetch("http://localhost");
151-
const updatedLoopbackPort = loopbackPortRegexp.exec(await res.text())?.[1];
155+
const state2 = await getState();
152156

153-
// Make sure a new port was allocated when `port: 0` was passed to `setOptions()`
154-
t.not(initialURL.port, "0");
155-
t.not(initialURL.port, updatedURL.port);
157+
// Make sure ports were reused when `port: 0` passed to `setOptions()`
158+
t.not(state1.url.port, "0");
159+
t.is(state1.url.port, state2.url.port);
160+
t.not(state1.inspectorUrl.port, "0");
161+
t.is(state1.inspectorUrl.port, state2.inspectorUrl.port);
156162

157163
// Make sure updating the host restarted the loopback server
158-
t.not(initialLoopbackPort, undefined);
159-
t.not(updatedLoopbackPort, undefined);
160-
t.not(initialLoopbackPort, updatedLoopbackPort);
164+
t.not(state1.loopbackPort, undefined);
165+
t.not(state2.loopbackPort, undefined);
166+
t.not(state1.loopbackPort, state2.loopbackPort);
167+
168+
// Make sure setting port to `undefined` always gives a new port, but keeps
169+
// existing loopback server
170+
opts.port = undefined;
171+
await mf.setOptions(opts);
172+
const state3 = await getState();
173+
t.not(state3.url.port, "0");
174+
t.not(state1.url.port, state3.url.port);
175+
t.is(state2.loopbackPort, state3.loopbackPort);
161176
});
162177

163178
test("Miniflare: routes to multiple workers with fallback", async (t) => {

0 commit comments

Comments
 (0)