Skip to content

Commit

Permalink
wip: recover from 'address in use' errors
Browse files Browse the repository at this point in the history
by trying to start on a random port
  • Loading branch information
RamIdeas committed Nov 15, 2023
1 parent 3bd19f5 commit 1348f52
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 37 deletions.
55 changes: 51 additions & 4 deletions packages/wrangler/src/api/startDevWorker/DevEnv.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assert from "node:assert";
import { EventEmitter } from "node:events";
import { logger } from "../../logger";
import { BundlerController } from "./BundlerController";
Expand Down Expand Up @@ -46,8 +47,14 @@ export class DevEnv extends EventEmitter {
controller.on("error", (event) => this.emitErrorEvent(event))
);

this.on("error", (event) => {
logger.error(event);
this.on("error", (event: ErrorEvent) => {
// TODO: when we're are comfortable with StartDevWorker/DevEnv stability,
// we can remove this handler and let the user handle the unknowable errors
// or let the process crash. For now, log them to stderr
// so we can identify knowable vs unknowable error candidates

logger.error(`Error in ${event.source}: ${event.reason}\n`, event.cause);
logger.debug("=> Error contextual data:", event.data);
});

config.on("configUpdate", (event) => {
Expand Down Expand Up @@ -98,8 +105,48 @@ export class DevEnv extends EventEmitter {
]);
}

emitErrorEvent(data: ErrorEvent) {
this.emit("error", data);
emitErrorEvent(ev: ErrorEvent) {
if (
ev.source === "ProxyController" &&
ev.reason === "Failed to start ProxyWorker or InspectorProxyWorker"
) {
assert(ev.data.config); // we must already have a `config` if we've already tried (and failed) to instantiate the ProxyWorker(s)

const { config } = ev.data;
const port = config.dev?.server?.port;
const inspectorPort = config.dev?.inspector?.port;
const randomPorts = [0, undefined];

// console.log({ port, inspectorPort, ev });
if (!randomPorts.includes(port) || !randomPorts.includes(inspectorPort)) {
// emit the event here while the ConfigController is unimplemented
// this will cause the ProxyController to try reinstantiating the ProxyWorker(s)
// TODO: change this to `this.config.updateOptions({ dev: { server: { port: 0 }, inspector: { port: 0 } } });` when the ConfigController is implemented
this.config.emitConfigUpdateEvent({
type: "configUpdate",
config: {
...config,
dev: {
...config.dev,
server: { ...config.dev?.server, port: 0 }, // override port
inspector: { ...config.dev?.inspector, port: 0 }, // override port
},
},
});
}
} else if (
ev.source === "ProxyController" &&
(ev.reason.startsWith("Failed to send message to") ||
ev.reason.startsWith("Could not connect to InspectorProxyWorker"))
) {
logger.debug(`Error in ${ev.source}: ${ev.reason}\n`, ev.cause);
logger.debug("=> Error contextual data:", ev.data);
}
// if other knowable + recoverable errors occur, handle them here
else {
// otherwise, re-emit the unknowable errors to the top-level
this.emit("error", ev);
}
}
}

Expand Down
65 changes: 42 additions & 23 deletions packages/wrangler/src/api/startDevWorker/ProxyController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,14 +140,15 @@ export class ProxyController extends EventEmitter {
proxyWorkerOptions
);

const willInstantiateMiniflareInstance =
!this.proxyWorker || proxyWorkerOptionsChanged;
this.proxyWorker ??= new Miniflare(proxyWorkerOptions);
this.proxyWorkerOptions = proxyWorkerOptions;

let setOptionsPromise: Promise<void> | undefined;
if (proxyWorkerOptionsChanged) {
logger.debug("ProxyWorker miniflare options changed, reinstantiating...");

setOptionsPromise = this.proxyWorker.setOptions(proxyWorkerOptions);
void this.proxyWorker.setOptions(proxyWorkerOptions);

// this creates a new .ready promise that will be resolved when both ProxyWorkers are ready
// it also respects any await-ers of the existing .ready promise
Expand All @@ -157,20 +158,21 @@ export class ProxyController extends EventEmitter {
// store the non-null versions for callbacks
const { proxyWorker } = this;

void Promise.all([
setOptionsPromise, // TODO: should this be awaited internally by mf.ready?
proxyWorker.ready,
this.reconnectInspectorProxyWorker(),
])
.then(() => {
this.emitReadyEvent(proxyWorker);
})
.catch((error) => {
this.emitErrorEvent(
"Failed to start ProxyWorker or InspectorProxyWorker",
error
);
});
if (willInstantiateMiniflareInstance) {
void Promise.all([
proxyWorker.ready,
this.reconnectInspectorProxyWorker(),
])
.then(() => {
this.emitReadyEvent(proxyWorker);
})
.catch((error) => {
this.emitErrorEvent(
"Failed to start ProxyWorker or InspectorProxyWorker",
error
);
});
}
}

async reconnectInspectorProxyWorker(): Promise<WebSocket | undefined> {
Expand Down Expand Up @@ -202,7 +204,11 @@ export class ProxyController extends EventEmitter {
const error = castErrorCause(cause);

this.inspectorProxyWorkerWebSocket?.reject(error);
this.emitErrorEvent("Could not connect to InspectorProxyWorker", error);
this.emitErrorEvent(
"Could not connect to InspectorProxyWorker " +
JSON.stringify({ webSocket, readyState: webSocket?.readyState }),
error
);
return;
}

Expand Down Expand Up @@ -238,10 +244,12 @@ export class ProxyController extends EventEmitter {
): Promise<void> {
if (this._torndown) return;

const ready = await this.proxyWorker?.ready.catch(() => undefined);
if (!ready) return;

try {
await this.runtimeMessageMutex.runWith(async () => {
assert(this.proxyWorker, "proxyWorker should already be instantiated");
await this.proxyWorker.ready;

return this.proxyWorker.dispatchFetch(
`http://dummy/cdn-cgi/ProxyWorker/${message.type}`,
Expand Down Expand Up @@ -275,14 +283,14 @@ export class ProxyController extends EventEmitter {
if (this._torndown) return;

try {
const websocket = await this.inspectorProxyWorkerWebSocket?.promise;
let websocket = await this.inspectorProxyWorkerWebSocket?.promise;
assert(websocket);

if (websocket.readyState >= WebSocket.READY_STATE_CLOSING) {
await this.reconnectInspectorProxyWorker();
websocket = await this.reconnectInspectorProxyWorker();
}

websocket.send(JSON.stringify(message));
websocket?.send(JSON.stringify(message));
} catch (cause) {
if (this._torndown) return;

Expand Down Expand Up @@ -465,8 +473,19 @@ export class ProxyController extends EventEmitter {
proxyData,
});
}
emitErrorEvent(reason: string, cause?: Error | SerializedError) {
this.emit("error", { source: "ProxyController", cause, reason });
emitErrorEvent(reason: string, cause: Error | SerializedError) {
const event: ErrorEvent = {
type: "error",
source: "ProxyController",
cause,
reason,
data: {
config: this.latestConfig,
bundle: this.latestBundle,
},
};

this.emit("error", event);
}

// *********************
Expand Down
28 changes: 18 additions & 10 deletions packages/wrangler/src/api/startDevWorker/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,27 @@ import type { Miniflare } from "miniflare";
export type TeardownEvent = {
type: "teardown";
};
export type ErrorEvent = {
export type ErrorEvent =
| BaseErrorEvent<
| "ConfigController"
| "BundlerController"
| "LocalRuntimeController"
| "RemoteRuntimeController"
| "ProxyWorker"
| "InspectorProxyWorker"
>
| BaseErrorEvent<
"ProxyController",
{ config?: StartDevWorkerOptions; bundle?: EsbuildBundle }
>;
export type BaseErrorEvent<Source = string, Data = undefined> = {
type: "error";
reason: string;
cause: Error;
source:
| "ConfigController"
| "BundlerController"
| "LocalRuntimeController"
| "RemoteRuntimeController"
| "ProxyController"
| "ProxyWorker"
| "InspectorProxyWorker";
cause: Error | SerializedError;
source: Source;
data: Data;
};

export function castErrorCause(cause: unknown) {
if (cause instanceof Error) return cause;

Expand Down

0 comments on commit 1348f52

Please sign in to comment.