n8n
Version:
n8n Workflow Automation Tool
368 lines • 18.4 kB
JavaScript
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
;