UNPKG

n8n

Version:

n8n Workflow Automation Tool

368 lines 18.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Worker = void 0; const typedi_1 = require("typedi"); const core_1 = require("@oclif/core"); const express_1 = __importDefault(require("express")); const http_1 = __importDefault(require("http")); const n8n_core_1 = require("n8n-core"); const n8n_workflow_1 = require("n8n-workflow"); const Db = __importStar(require("../Db")); const ResponseHelper = __importStar(require("../ResponseHelper")); const WebhookHelpers = __importStar(require("../WebhookHelpers")); const WorkflowExecuteAdditionalData = __importStar(require("../WorkflowExecuteAdditionalData")); const config_1 = __importDefault(require("../config")); const Queue_1 = require("../Queue"); const constants_1 = require("../constants"); const execution_repository_1 = require("../databases/repositories/execution.repository"); const workflow_repository_1 = require("../databases/repositories/workflow.repository"); const CredentialsOverwrites_1 = require("../CredentialsOverwrites"); const middlewares_1 = require("../middlewares"); const MessageEventBus_1 = require("../eventbus/MessageEventBus/MessageEventBus"); const EventMessageGeneric_1 = require("../eventbus/EventMessageClasses/EventMessageGeneric"); const orchestration_handler_worker_service_1 = require("../services/orchestration/worker/orchestration.handler.worker.service"); const orchestration_worker_service_1 = require("../services/orchestration/worker/orchestration.worker.service"); const service_unavailable_error_1 = require("../errors/response-errors/service-unavailable.error"); const BaseCommand_1 = require("./BaseCommand"); const max_stalled_count_error_1 = require("../errors/max-stalled-count.error"); class Worker extends BaseCommand_1.BaseCommand { async stopProcess() { var _a; this.logger.info('Stopping n8n...'); await Worker.jobQueue.pause({ isLocal: true, doNotWaitActive: true }); try { await ((_a = this.externalHooks) === null || _a === void 0 ? void 0 : _a.run('n8n.stop', [])); const hardStopTimeMs = Date.now() + this.gracefulShutdownTimeoutInS * 1000; let count = 0; while (Object.keys(Worker.runningJobs).length !== 0) { if (count++ % 4 === 0) { const waitLeft = Math.ceil((hardStopTimeMs - Date.now()) / 1000); this.logger.info(`Waiting for ${Object.keys(Worker.runningJobs).length} active executions to finish... (max wait ${waitLeft} more seconds)`); } await (0, n8n_workflow_1.sleep)(500); } } catch (error) { await this.exitWithCrash('There was an error shutting down n8n.', error); } await this.exitSuccessFully(); } async runJob(job, nodeTypes) { var _a, _b, _c, _d; const { executionId, loadStaticData } = job.data; const executionRepository = typedi_1.Container.get(execution_repository_1.ExecutionRepository); const fullExecutionData = await executionRepository.findSingleExecution(executionId, { includeData: true, unflattenData: true, }); if (!fullExecutionData) { this.logger.error(`Worker failed to find data of execution "${executionId}" in database. Cannot continue.`, { executionId }); throw new n8n_workflow_1.ApplicationError('Unable to find data of execution in database. Aborting execution.', { extra: { executionId } }); } const workflowId = fullExecutionData.workflowData.id; this.logger.info(`Start job: ${job.id} (Workflow ID: ${workflowId} | Execution: ${executionId})`); await executionRepository.updateStatus(executionId, 'running'); let { staticData } = fullExecutionData.workflowData; if (loadStaticData) { const workflowData = await typedi_1.Container.get(workflow_repository_1.WorkflowRepository).findOne({ select: ['id', 'staticData'], where: { id: workflowId, }, }); if (workflowData === null) { this.logger.error('Worker execution failed because workflow could not be found in database.', { workflowId, executionId }); throw new n8n_workflow_1.ApplicationError('Workflow could not be found', { extra: { workflowId } }); } staticData = workflowData.staticData; } const workflowSettings = (_a = fullExecutionData.workflowData.settings) !== null && _a !== void 0 ? _a : {}; let workflowTimeout = (_b = workflowSettings.executionTimeout) !== null && _b !== void 0 ? _b : config_1.default.getEnv('executions.timeout'); let executionTimeoutTimestamp; if (workflowTimeout > 0) { workflowTimeout = Math.min(workflowTimeout, config_1.default.getEnv('executions.maxTimeout')); executionTimeoutTimestamp = Date.now() + workflowTimeout * 1000; } const workflow = new n8n_workflow_1.Workflow({ id: workflowId, name: fullExecutionData.workflowData.name, nodes: fullExecutionData.workflowData.nodes, connections: fullExecutionData.workflowData.connections, active: fullExecutionData.workflowData.active, nodeTypes, staticData, settings: fullExecutionData.workflowData.settings, }); const additionalData = await WorkflowExecuteAdditionalData.getBase(undefined, undefined, executionTimeoutTimestamp); additionalData.hooks = WorkflowExecuteAdditionalData.getWorkflowHooksWorkerExecuter(fullExecutionData.mode, job.data.executionId, fullExecutionData.workflowData, { retryOf: fullExecutionData.retryOf, }); additionalData.hooks.hookFunctions.sendResponse = [ async (response) => { const progress = { executionId, response: WebhookHelpers.encodeWebhookResponse(response), }; await job.progress(progress); }, ]; additionalData.executionId = executionId; additionalData.setExecutionStatus = (status) => { this.logger.debug(`Queued worker execution status for ${executionId} is "${status}"`); }; let workflowExecute; let workflowRun; if (fullExecutionData.data !== undefined) { workflowExecute = new n8n_core_1.WorkflowExecute(additionalData, fullExecutionData.mode, fullExecutionData.data); workflowRun = workflowExecute.processRunExecutionData(workflow); } else { workflowExecute = new n8n_core_1.WorkflowExecute(additionalData, fullExecutionData.mode); workflowRun = workflowExecute.run(workflow); } Worker.runningJobs[job.id] = workflowRun; Worker.runningJobsSummary[job.id] = { jobId: job.id.toString(), executionId, workflowId: (_c = fullExecutionData.workflowId) !== null && _c !== void 0 ? _c : '', workflowName: fullExecutionData.workflowData.name, mode: fullExecutionData.mode, startedAt: fullExecutionData.startedAt, retryOf: (_d = fullExecutionData.retryOf) !== null && _d !== void 0 ? _d : '', status: fullExecutionData.status, }; await workflowRun; delete Worker.runningJobs[job.id]; delete Worker.runningJobsSummary[job.id]; return { success: true, }; } constructor(argv, cmdConfig) { super(argv, cmdConfig); if (!process.env.N8N_ENCRYPTION_KEY) { throw new n8n_workflow_1.ApplicationError('Missing encryption key. Worker started without the required N8N_ENCRYPTION_KEY env var. More information: https://docs.n8n.io/hosting/configuration/configuration-examples/encryption-key/'); } this.setInstanceType('worker'); this.setInstanceQueueModeId(); } async init() { const { QUEUE_WORKER_TIMEOUT } = process.env; if (QUEUE_WORKER_TIMEOUT) { this.gracefulShutdownTimeoutInS = parseInt(QUEUE_WORKER_TIMEOUT, 10) || config_1.default.default('queue.bull.gracefulShutdownTimeout'); this.logger.warn('QUEUE_WORKER_TIMEOUT has been deprecated. Rename it to N8N_GRACEFUL_SHUTDOWN_TIMEOUT.'); } await this.initCrashJournal(); this.logger.debug('Starting n8n worker...'); this.logger.debug(`Queue mode id: ${this.queueModeId}`); await super.init(); await this.initLicense(); this.logger.debug('License init complete'); await this.initBinaryDataService(); this.logger.debug('Binary data service init complete'); await this.initExternalHooks(); this.logger.debug('External hooks init complete'); await this.initExternalSecrets(); this.logger.debug('External secrets init complete'); await this.initEventBus(); this.logger.debug('Event bus init complete'); await this.initQueue(); this.logger.debug('Queue init complete'); await this.initOrchestration(); this.logger.debug('Orchestration init complete'); await typedi_1.Container.get(orchestration_worker_service_1.OrchestrationWorkerService).publishToEventLog(new EventMessageGeneric_1.EventMessageGeneric({ eventName: 'n8n.worker.started', payload: { workerId: this.queueModeId, }, })); } async initEventBus() { await typedi_1.Container.get(MessageEventBus_1.MessageEventBus).initialize({ workerId: this.queueModeId, }); } async initOrchestration() { await typedi_1.Container.get(orchestration_worker_service_1.OrchestrationWorkerService).init(); await typedi_1.Container.get(orchestration_handler_worker_service_1.OrchestrationHandlerWorkerService).initWithOptions({ queueModeId: this.queueModeId, redisPublisher: typedi_1.Container.get(orchestration_worker_service_1.OrchestrationWorkerService).redisPublisher, getRunningJobIds: () => Object.keys(Worker.runningJobs), getRunningJobsSummary: () => Object.values(Worker.runningJobsSummary), }); } async initQueue() { const { flags } = await this.parse(Worker); const redisConnectionTimeoutLimit = config_1.default.getEnv('queue.bull.redis.timeoutThreshold'); this.logger.debug(`Opening Redis connection to listen to messages with timeout ${redisConnectionTimeoutLimit}`); Worker.jobQueue = typedi_1.Container.get(Queue_1.Queue); await Worker.jobQueue.init(); this.logger.debug('Queue singleton ready'); void Worker.jobQueue.process(flags.concurrency, async (job) => await this.runJob(job, this.nodeTypes)); Worker.jobQueue.getBullObjectInstance().on('global:progress', (jobId, progress) => { if (progress === -1) { if (Worker.runningJobs[jobId] !== undefined) { Worker.runningJobs[jobId].cancel(); delete Worker.runningJobs[jobId]; } } }); let lastTimer = 0; let cumulativeTimeout = 0; Worker.jobQueue.getBullObjectInstance().on('error', (error) => { if (error.toString().includes('ECONNREFUSED')) { const now = Date.now(); if (now - lastTimer > 30000) { lastTimer = now; cumulativeTimeout = 0; } else { cumulativeTimeout += now - lastTimer; lastTimer = now; if (cumulativeTimeout > redisConnectionTimeoutLimit) { this.logger.error(`Unable to connect to Redis after ${redisConnectionTimeoutLimit}. Exiting process.`); process.exit(1); } } this.logger.warn('Redis unavailable - trying to reconnect...'); } else if (error.toString().includes('Error initializing Lua scripts')) { this.logger.error('Error initializing worker.'); process.exit(2); } else { this.logger.error('Error from queue: ', error); if (error.message.includes('job stalled more than maxStalledCount')) { throw new max_stalled_count_error_1.MaxStalledCountError(error); } throw error; } }); } async setupHealthMonitor() { var _a; const port = config_1.default.getEnv('queue.health.port'); const app = (0, express_1.default)(); app.disable('x-powered-by'); const server = http_1.default.createServer(app); app.get('/healthz', async (_req, res) => { this.logger.debug('Health check started!'); const connection = Db.getConnection(); try { if (!connection.isInitialized) { throw new n8n_workflow_1.ApplicationError('No active database connection'); } await connection.query('SELECT 1'); } catch (e) { this.logger.error('No Database connection!', e); const error = new service_unavailable_error_1.ServiceUnavailableError('No Database connection!'); return ResponseHelper.sendErrorResponse(res, error); } try { await Worker.jobQueue.ping(); } catch (e) { this.logger.error('No Redis connection!', e); const error = new service_unavailable_error_1.ServiceUnavailableError('No Redis connection!'); return ResponseHelper.sendErrorResponse(res, error); } const responseData = { status: 'ok', }; this.logger.debug('Health check completed successfully!'); ResponseHelper.sendSuccessResponse(res, responseData, true, 200); }); let presetCredentialsLoaded = false; const endpointPresetCredentials = config_1.default.getEnv('credentials.overwrite.endpoint'); if (endpointPresetCredentials !== '') { app.post(`/${endpointPresetCredentials}`, middlewares_1.rawBodyReader, middlewares_1.bodyParser, async (req, res) => { if (!presetCredentialsLoaded) { const body = req.body; if (req.contentType !== 'application/json') { ResponseHelper.sendErrorResponse(res, new Error('Body must be a valid JSON, make sure the content-type is application/json')); return; } typedi_1.Container.get(CredentialsOverwrites_1.CredentialsOverwrites).setData(body); presetCredentialsLoaded = true; ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); } else { ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once')); } }); } server.on('error', (error) => { if (error.code === 'EADDRINUSE') { this.logger.error(`n8n's port ${port} is already in use. Do you have the n8n main process running on that port?`); process.exit(1); } }); await new Promise((resolve) => server.listen(port, () => resolve())); await ((_a = this.externalHooks) === null || _a === void 0 ? void 0 : _a.run('worker.ready')); this.logger.info(`\nn8n worker health check via, port ${port}`); } async run() { const { flags } = await this.parse(Worker); this.logger.info('\nn8n worker is now ready'); this.logger.info(` * Version: ${constants_1.N8N_VERSION}`); this.logger.info(` * Concurrency: ${flags.concurrency}`); this.logger.info(''); if (config_1.default.getEnv('queue.health.active')) { await this.setupHealthMonitor(); } if (process.stdout.isTTY) { process.stdin.setRawMode(true); process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.on('data', (key) => { if (key.charCodeAt(0) === 3) process.kill(process.pid, 'SIGINT'); }); } await new Promise(() => { }); } async catch(error) { await this.exitWithCrash('Worker exiting due to an error.', error); } } exports.Worker = Worker; Worker.description = '\nStarts a n8n worker'; Worker.examples = ['$ n8n worker --concurrency=5']; Worker.flags = { help: core_1.Flags.help({ char: 'h' }), concurrency: core_1.Flags.integer({ default: 10, description: 'How many jobs can run in parallel.', }), }; Worker.runningJobs = {}; Worker.runningJobsSummary = {}; //# sourceMappingURL=worker.js.map