inngest
Version:
Official SDK for Inngest.com. Inngest is the reliability layer for modern applications. Inngest combines durable execution, events, and queues into a zero-infra platform with built-in observability.
245 lines (243 loc) • 8.04 kB
JavaScript
import { internalLoggerSymbol } from "../../../Inngest.js";
import { GatewayExecutorRequestData, SDKResponse } from "../../../../proto/src/components/connect/protobuf/connect.js";
import { ConnectionState } from "../../types.js";
import { BaseStrategy } from "../core/BaseStrategy.js";
import { fileURLToPath } from "node:url";
import { Worker } from "node:worker_threads";
import { dirname, extname, join } from "node:path";
//#region src/components/connect/strategies/workerThread/index.ts
/**
* Worker thread connection strategy.
*
* This strategy runs the WebSocket connection, heartbeater, and lease extender
* in a separate worker thread. Userland code execution still happens in the
* main thread.
*/
const maxConsecutiveCrashes = 10;
const baseBackoffMs = 500;
const maxBackoffMs = 3e4;
/**
* Worker thread connection strategy.
*
* This strategy runs the WebSocket connection, heartbeater, and lease extender
* in a separate Node.js worker thread. This prevents blocked user code from
* interfering with connection health.
*/
var WorkerThreadStrategy = class extends BaseStrategy {
config;
worker;
consecutiveCrashes = 0;
_connectionId;
constructor(config) {
const primaryApp = config.options.apps[0];
if (!primaryApp) throw new Error("No apps");
super({ logger: primaryApp.client[internalLoggerSymbol] });
this.config = config;
}
get connectionId() {
return this._connectionId;
}
async close() {
this.cleanupShutdown();
this.setClosing();
this.internalLogger.debug("Closing worker thread connection");
if (this.worker) {
this.sendToWorker({ type: "CLOSE" });
await new Promise((resolve) => {
if (!this.worker) {
resolve();
return;
}
const timeout = setTimeout(() => {
this.internalLogger.debug("Worker close timeout, terminating");
this.worker?.terminate();
resolve();
}, 3e4);
this.worker.once("exit", () => {
clearTimeout(timeout);
resolve();
});
});
this.worker = void 0;
}
this.setClosed();
this.internalLogger.debug("Worker thread connection closed");
}
async connect(attempt = 0) {
this.throwIfClosingOrClosed();
this.internalLogger.debug({ attempt }, "Starting worker thread connection");
this.setupShutdownSignalIfConfigured(this.config.options.handleShutdownSignals);
await this.createWorker();
const serializableConfig = await this.buildSerializableConfig();
this.sendToWorker({
type: "INIT",
config: serializableConfig
});
await new Promise((resolve, reject) => {
if (!this.worker) {
reject(/* @__PURE__ */ new Error("Worker not created"));
return;
}
const cleanup = () => {
this.worker?.off("message", handleMessage);
this.worker?.off("exit", handleExit);
};
const handleMessage = (msg) => {
if (msg.type === "CONNECTION_READY") {
this._connectionId = msg.connectionId;
cleanup();
resolve();
} else if (msg.type === "ERROR" && msg.fatal) {
cleanup();
reject(new Error(msg.error));
}
};
const handleExit = (code) => {
cleanup();
reject(/* @__PURE__ */ new Error(`Worker thread exited with code ${code} during connect`));
};
this.worker.on("message", handleMessage);
this.worker.on("exit", handleExit);
this.sendToWorker({
type: "CONNECT",
attempt
});
});
}
async createWorker() {
const currentFilePath = fileURLToPath(import.meta.url);
const ext = extname(currentFilePath);
const runnerPath = join(dirname(currentFilePath), `runner${ext}`);
this.internalLogger.debug({ runnerPath }, "Creating worker thread");
this.worker = new Worker(runnerPath, { env: process.env });
this.worker.on("message", (msg) => {
this.handleWorkerMessage(msg);
});
this.worker.on("error", (err) => {
this.internalLogger.debug({ err }, "Worker error");
this._state = ConnectionState.RECONNECTING;
});
this.worker.on("exit", (code) => {
this.internalLogger.debug({ code }, "Worker exited");
if (this._state === ConnectionState.CLOSING || this._state === ConnectionState.CLOSED) return;
this.consecutiveCrashes++;
this._state = ConnectionState.RECONNECTING;
if (this.consecutiveCrashes > maxConsecutiveCrashes) {
this.internalLogger.error({ consecutiveCrashes: this.consecutiveCrashes }, "Worker thread crashed consecutively, giving up");
return;
}
const backoff = Math.min(baseBackoffMs * 2 ** (this.consecutiveCrashes - 1), maxBackoffMs);
this.internalLogger.warn({
consecutiveCrashes: this.consecutiveCrashes,
backoffMs: backoff
}, "Respawning worker after backoff");
setTimeout(() => {
if (this._state === ConnectionState.CLOSING || this._state === ConnectionState.CLOSED) return;
this.createWorker().then(async () => {
const config = await this.buildSerializableConfig();
this.sendToWorker({
type: "INIT",
config
});
this.sendToWorker({
type: "CONNECT",
attempt: 0
});
}).catch((err) => {
this.internalLogger.debug({ err }, "Failed to recreate worker");
});
}, backoff);
});
}
handleWorkerMessage(msg) {
switch (msg.type) {
case "STATE_CHANGE":
this._state = msg.state;
this.internalLogger.debug({ state: msg.state }, "State changed");
break;
case "CONNECTION_READY":
this._connectionId = msg.connectionId;
this.consecutiveCrashes = 0;
this.internalLogger.debug({ connectionId: msg.connectionId }, "Connection ready");
break;
case "ERROR":
if (msg.fatal) this.internalLogger.error({ errorMessage: msg.error }, "Fatal error from worker");
else this.internalLogger.error({ errorMessage: msg.error }, "Worker error");
break;
case "EXECUTION_REQUEST":
this.handleExecutionRequest(msg.requestId, msg.request);
break;
case "CLOSED":
this._state = ConnectionState.CLOSED;
this.resolveClosingPromise?.();
break;
case "LOG":
this.handleWorkerLog(msg.level, msg.message, msg.data);
break;
}
}
handleWorkerLog(level, message, data) {
if (data) this.internalLogger[level](data, message);
else this.internalLogger[level](message);
}
async handleExecutionRequest(requestId, requestBytes) {
try {
const gatewayExecutorRequest = GatewayExecutorRequestData.decode(requestBytes);
const requestHandler = this.config.requestHandlers[gatewayExecutorRequest.appName];
if (!requestHandler) {
this.internalLogger.debug({ appName: gatewayExecutorRequest.appName }, "No handler for app");
this.sendToWorker({
type: "EXECUTION_ERROR",
requestId,
error: `No handler for app: ${gatewayExecutorRequest.appName}`
});
return;
}
const response = await requestHandler(gatewayExecutorRequest);
const responseBytes = SDKResponse.encode(response).finish();
this.sendToWorker({
type: "EXECUTION_RESPONSE",
requestId,
response: responseBytes
});
} catch (err) {
let error;
if (err instanceof Error) error = err;
else error = new Error(String(err));
this.internalLogger.debug({
err: error,
requestId
}, "Execution error");
this.sendToWorker({
type: "EXECUTION_ERROR",
requestId,
error: err instanceof Error ? err.message : "Unknown error"
});
}
}
sendToWorker(msg) {
if (!this.worker) {
this.internalLogger.error("Cannot send message, no worker");
return;
}
this.worker.postMessage(msg);
}
async buildSerializableConfig() {
return {
apiBaseUrl: this.config.apiBaseUrl,
appIds: Object.keys(this.config.requestHandlers),
connectionData: this.config.connectionData,
envName: this.config.envName,
gatewayUrl: this.config.options.gatewayUrl,
handleShutdownSignals: this.config.options.handleShutdownSignals,
hashedFallbackKey: this.config.hashedFallbackKey,
hashedSigningKey: this.config.hashedSigningKey,
instanceId: this.config.options.instanceId,
maxWorkerConcurrency: this.config.options.maxWorkerConcurrency,
mode: this.config.mode
};
}
};
//#endregion
export { WorkerThreadStrategy };
//# sourceMappingURL=index.js.map