UNPKG

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
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