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.

246 lines (244 loc) • 8.4 kB
const require_rolldown_runtime = require('../../../../_virtual/rolldown_runtime.cjs'); const require_Inngest = require('../../../Inngest.cjs'); const require_connect = require('../../../../proto/src/components/connect/protobuf/connect.cjs'); const require_types = require('../../types.cjs'); const require_BaseStrategy = require('../core/BaseStrategy.cjs'); let node_url = require("node:url"); let node_worker_threads = require("node:worker_threads"); let node_path = require("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 require_BaseStrategy.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[require_Inngest.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 = (0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href); const ext = (0, node_path.extname)(currentFilePath); const runnerPath = (0, node_path.join)((0, node_path.dirname)(currentFilePath), `runner${ext}`); this.internalLogger.debug({ runnerPath }, "Creating worker thread"); this.worker = new node_worker_threads.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 = require_types.ConnectionState.RECONNECTING; }); this.worker.on("exit", (code) => { this.internalLogger.debug({ code }, "Worker exited"); if (this._state === require_types.ConnectionState.CLOSING || this._state === require_types.ConnectionState.CLOSED) return; this.consecutiveCrashes++; this._state = require_types.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 === require_types.ConnectionState.CLOSING || this._state === require_types.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 = require_types.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 = require_connect.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 = require_connect.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 exports.WorkerThreadStrategy = WorkerThreadStrategy; //# sourceMappingURL=index.cjs.map