n8n
Version:
n8n Workflow Automation Tool
229 lines • 11.3 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExecutionRecoveryService = void 0;
const backend_common_1 = require("@n8n/backend-common");
const config_1 = require("@n8n/config");
const db_1 = require("@n8n/db");
const db_2 = require("@n8n/db");
const di_1 = require("@n8n/di");
const permissions_1 = require("@n8n/permissions");
const n8n_core_1 = require("n8n-core");
const n8n_workflow_1 = require("n8n-workflow");
const constants_1 = require("../constants");
const node_crashed_error_1 = require("../errors/node-crashed.error");
const workflow_crashed_error_1 = require("../errors/workflow-crashed.error");
const execution_lifecycle_hooks_1 = require("../execution-lifecycle/execution-lifecycle-hooks");
const push_1 = require("../push");
const ownership_service_1 = require("../services/ownership.service");
const user_management_mailer_1 = require("../user-management/email/user-management-mailer");
let ExecutionRecoveryService = class ExecutionRecoveryService {
constructor(logger, instanceSettings, push, executionRepository, executionsConfig, workflowRepository, userManagementMailer, ownershipService, projectRelationRepository) {
this.logger = logger;
this.instanceSettings = instanceSettings;
this.push = push;
this.executionRepository = executionRepository;
this.executionsConfig = executionsConfig;
this.workflowRepository = workflowRepository;
this.userManagementMailer = userManagementMailer;
this.ownershipService = ownershipService;
this.projectRelationRepository = projectRelationRepository;
}
async autoDeactivateWorkflowsIfNeeded(workflowIds) {
for (const workflowId of workflowIds) {
const maxLastExecutions = this.executionsConfig.recovery.maxLastExecutions;
const lastExecutions = await this.executionRepository.findMultipleExecutions({
select: ['id', 'status'],
where: { workflowId },
order: { startedAt: 'DESC' },
take: maxLastExecutions,
});
const numberOfCrashedExecutions = lastExecutions.filter((e) => e.status === 'crashed').length;
if (lastExecutions.length >= maxLastExecutions &&
lastExecutions.length === numberOfCrashedExecutions) {
const workflow = await this.workflowRepository.findOne({ where: { id: workflowId } });
if (!workflow) {
this.logger.warn(`Workflow ${workflowId} not found, skipping workflow auto-deactivation`);
continue;
}
if (workflow.activeVersionId !== null) {
await this.workflowRepository.updateActiveState(workflowId, false);
this.logger.warn(`Autodeactivated workflow ${workflowId} due to too many crashed executions.`);
const recipient = await this.getAutodeactivationRecipient(workflow);
await this.userManagementMailer.notifyWorkflowAutodeactivated({
recipient,
workflow,
});
this.push.once('editorUiConnected', async () => {
await (0, n8n_workflow_1.sleep)(1000);
this.push.broadcast({ type: 'workflowAutoDeactivated', data: { workflowId } });
});
}
await this.executionRepository.update({ workflowId, status: (0, db_1.In)(['running', 'new']) }, { status: 'crashed', stoppedAt: new Date() });
}
}
}
async recoverFromLogs(executionId, messages) {
if (this.instanceSettings.isFollower)
return;
const amendedExecution = await this.amend(executionId, messages);
if (!amendedExecution)
return null;
this.logger.info('[Recovery] Logs available, amended execution', {
executionId: amendedExecution.id,
});
await this.executionRepository.updateExistingExecution(executionId, amendedExecution);
await this.runHooks(amendedExecution);
this.push.once('editorUiConnected', async () => {
await (0, n8n_workflow_1.sleep)(1000);
this.push.broadcast({ type: 'executionRecovered', data: { executionId } });
});
return amendedExecution;
}
async amend(executionId, messages) {
if (messages.length === 0)
return await this.amendWithoutLogs(executionId);
const { nodeMessages, workflowMessages } = this.toRelevantMessages(messages);
if (nodeMessages.length === 0)
return null;
const execution = await this.executionRepository.findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (!execution ||
(['success', 'error', 'canceled'].includes(execution.status) && execution.data)) {
return null;
}
const runExecutionData = execution.data ?? { resultData: { runData: {} } };
let lastNodeRunTimestamp;
for (const node of execution.workflowData.nodes) {
const nodeStartedMessage = nodeMessages.find((m) => m.payload.nodeName === node.name && m.eventName === 'n8n.node.started');
if (!nodeStartedMessage)
continue;
const nodeHasRunData = runExecutionData.resultData.runData[node.name] !== undefined;
if (nodeHasRunData)
continue;
const nodeFinishedMessage = nodeMessages.find((m) => m.payload.nodeName === node.name && m.eventName === 'n8n.node.finished');
const taskData = {
startTime: nodeStartedMessage.ts.toUnixInteger(),
executionIndex: 0,
executionTime: -1,
source: [null],
};
if (nodeFinishedMessage) {
taskData.executionStatus = 'success';
taskData.data ??= constants_1.ARTIFICIAL_TASK_DATA;
taskData.executionTime = nodeFinishedMessage.ts.diff(nodeStartedMessage.ts).toMillis();
lastNodeRunTimestamp = nodeFinishedMessage.ts;
}
else {
taskData.executionStatus = 'crashed';
taskData.error = new node_crashed_error_1.NodeCrashedError(node);
taskData.executionTime = 0;
runExecutionData.resultData.error = new workflow_crashed_error_1.WorkflowCrashedError();
lastNodeRunTimestamp = nodeStartedMessage.ts;
}
runExecutionData.resultData.lastNodeExecuted = node.name;
runExecutionData.resultData.runData[node.name] = [taskData];
}
return {
...execution,
status: execution.status === 'error' ? 'error' : 'crashed',
stoppedAt: this.toStoppedAt(lastNodeRunTimestamp, workflowMessages),
data: runExecutionData,
};
}
async amendWithoutLogs(executionId) {
const exists = await this.executionRepository.exists({ where: { id: executionId } });
if (!exists)
return null;
await this.executionRepository.markAsCrashed(executionId);
const execution = await this.executionRepository.findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
return execution ?? null;
}
toRelevantMessages(messages) {
return messages.reduce((acc, cur) => {
if (cur.eventName.startsWith('n8n.node.')) {
acc.nodeMessages.push(cur);
}
else if (cur.eventName.startsWith('n8n.workflow.')) {
acc.workflowMessages.push(cur);
}
return acc;
}, { nodeMessages: [], workflowMessages: [] });
}
toStoppedAt(timestamp, messages) {
if (timestamp)
return timestamp.toJSDate();
const WORKFLOW_END_EVENTS = new Set([
'n8n.workflow.success',
'n8n.workflow.crashed',
'n8n.workflow.failed',
]);
return (messages.find((m) => WORKFLOW_END_EVENTS.has(m.eventName)) ??
messages.find((m) => m.eventName === 'n8n.workflow.started'))?.ts.toJSDate();
}
async runHooks(execution) {
execution.data ??= (0, n8n_workflow_1.createEmptyRunExecutionData)();
const lifecycleHooks = (0, execution_lifecycle_hooks_1.getLifecycleHooksForRegularMain)({
userId: '',
workflowData: execution.workflowData,
executionMode: execution.mode,
executionData: execution.data,
runData: execution.data.resultData.runData,
retryOf: execution.retryOf ?? undefined,
}, execution.id);
const run = {
data: execution.data,
finished: false,
mode: execution.mode,
waitTill: execution.waitTill ?? undefined,
startedAt: execution.startedAt,
stoppedAt: execution.stoppedAt,
status: execution.status,
};
await lifecycleHooks.runHook('workflowExecuteAfter', [run]);
}
async getAutodeactivationRecipient(workflow) {
const project = await this.ownershipService.getWorkflowProjectCached(workflow.id);
const roleSlug = project.type === 'team' ? permissions_1.PROJECT_ADMIN_ROLE_SLUG : permissions_1.PROJECT_OWNER_ROLE_SLUG;
const projectRelations = await this.projectRelationRepository.find({
where: {
projectId: project.id,
role: { slug: roleSlug },
},
relations: { user: true },
});
if (projectRelations.length > 0) {
return projectRelations[0].user;
}
else {
return await this.ownershipService.getInstanceOwner();
}
}
};
exports.ExecutionRecoveryService = ExecutionRecoveryService;
exports.ExecutionRecoveryService = ExecutionRecoveryService = __decorate([
(0, di_1.Service)(),
__metadata("design:paramtypes", [backend_common_1.Logger,
n8n_core_1.InstanceSettings,
push_1.Push,
db_2.ExecutionRepository,
config_1.ExecutionsConfig,
db_2.WorkflowRepository,
user_management_mailer_1.UserManagementMailer,
ownership_service_1.OwnershipService,
db_1.ProjectRelationRepository])
], ExecutionRecoveryService);
//# sourceMappingURL=execution-recovery.service.js.map