refactor(core): Use DI for eventBus code - Part 1 (no-changelog) (#8434)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-01-26 12:21:15 +01:00 committed by GitHub
parent 3cc0f81c02
commit 7c49004018
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 399 additions and 355 deletions

View file

@ -1,5 +1,6 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { snakeCase } from 'change-case'; import { snakeCase } from 'change-case';
import { get as pslGet } from 'psl';
import type { import type {
AuthenticationMethod, AuthenticationMethod,
ExecutionStatus, ExecutionStatus,
@ -10,7 +11,15 @@ import type {
WorkflowExecuteMode, WorkflowExecuteMode,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { TelemetryHelpers } from 'n8n-workflow'; import { TelemetryHelpers } from 'n8n-workflow';
import { get as pslGet } from 'psl'; import { InstanceSettings } from 'n8n-core';
import { N8N_VERSION } from '@/constants';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { GlobalRole, User } from '@db/entities/User';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { MessageEventBus, type EventPayloadWorkflow } from '@/eventbus';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import type { import type {
IDiagnosticInfo, IDiagnosticInfo,
ITelemetryUserDeletionData, ITelemetryUserDeletionData,
@ -18,18 +27,9 @@ import type {
IExecutionTrackProperties, IExecutionTrackProperties,
IWorkflowExecutionDataProcess, IWorkflowExecutionDataProcess,
} from '@/Interfaces'; } from '@/Interfaces';
import { Telemetry } from '@/telemetry';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import { eventBus } from './eventbus';
import { EventsService } from '@/services/events.service'; import { EventsService } from '@/services/events.service';
import type { GlobalRole, User } from '@db/entities/User';
import { N8N_VERSION } from '@/constants';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import type { ExecutionMetadata } from '@db/entities/ExecutionMetadata'; import { Telemetry } from '@/telemetry';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import type { EventPayloadWorkflow } from './eventbus/EventMessageClasses/EventMessageWorkflow';
import { determineFinalExecutionStatus } from './executionLifecycleHooks/shared/sharedHookFunctions';
import { InstanceSettings } from 'n8n-core';
function userToPayload(user: User): { function userToPayload(user: User): {
userId: string; userId: string;
@ -55,6 +55,7 @@ export class InternalHooks {
private sharedWorkflowRepository: SharedWorkflowRepository, private sharedWorkflowRepository: SharedWorkflowRepository,
eventsService: EventsService, eventsService: EventsService,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly eventBus: MessageEventBus,
) { ) {
eventsService.on( eventsService.on(
'telemetry.onFirstProductionWorkflowSuccess', 'telemetry.onFirstProductionWorkflowSuccess',
@ -122,7 +123,7 @@ export class InternalHooks {
async onWorkflowCreated(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void> { async onWorkflowCreated(user: User, workflow: IWorkflowBase, publicApi: boolean): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes); const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.workflow.created', eventName: 'n8n.audit.workflow.created',
payload: { payload: {
...userToPayload(user), ...userToPayload(user),
@ -141,7 +142,7 @@ export class InternalHooks {
async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> { async onWorkflowDeleted(user: User, workflowId: string, publicApi: boolean): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.workflow.deleted', eventName: 'n8n.audit.workflow.deleted',
payload: { payload: {
...userToPayload(user), ...userToPayload(user),
@ -173,7 +174,7 @@ export class InternalHooks {
} }
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.workflow.updated', eventName: 'n8n.audit.workflow.updated',
payload: { payload: {
...userToPayload(user), ...userToPayload(user),
@ -201,7 +202,7 @@ export class InternalHooks {
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName); const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName);
void eventBus.sendNodeEvent({ void this.eventBus.sendNodeEvent({
eventName: 'n8n.node.started', eventName: 'n8n.node.started',
payload: { payload: {
executionId, executionId,
@ -219,7 +220,7 @@ export class InternalHooks {
nodeName: string, nodeName: string,
): Promise<void> { ): Promise<void> {
const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName); const nodeInWorkflow = workflow.nodes.find((node) => node.name === nodeName);
void eventBus.sendNodeEvent({ void this.eventBus.sendNodeEvent({
eventName: 'n8n.node.finished', eventName: 'n8n.node.finished',
payload: { payload: {
executionId, executionId,
@ -255,7 +256,7 @@ export class InternalHooks {
workflowName: (data as IWorkflowBase).name, workflowName: (data as IWorkflowBase).name,
}; };
} }
void eventBus.sendWorkflowEvent({ void this.eventBus.sendWorkflowEvent({
eventName: 'n8n.workflow.started', eventName: 'n8n.workflow.started',
payload, payload,
}); });
@ -277,7 +278,7 @@ export class InternalHooks {
} catch {} } catch {}
void Promise.all([ void Promise.all([
eventBus.sendWorkflowEvent({ this.eventBus.sendWorkflowEvent({
eventName: 'n8n.workflow.crashed', eventName: 'n8n.workflow.crashed',
payload: { payload: {
executionId, executionId,
@ -435,11 +436,11 @@ export class InternalHooks {
}; };
promises.push( promises.push(
telemetryProperties.success telemetryProperties.success
? eventBus.sendWorkflowEvent({ ? this.eventBus.sendWorkflowEvent({
eventName: 'n8n.workflow.success', eventName: 'n8n.workflow.success',
payload: sharedEventPayload, payload: sharedEventPayload,
}) })
: eventBus.sendWorkflowEvent({ : this.eventBus.sendWorkflowEvent({
eventName: 'n8n.workflow.failed', eventName: 'n8n.workflow.failed',
payload: { payload: {
...sharedEventPayload, ...sharedEventPayload,
@ -480,7 +481,7 @@ export class InternalHooks {
publicApi: boolean; publicApi: boolean;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.deleted', eventName: 'n8n.audit.user.deleted',
payload: { payload: {
...userToPayload(userDeletionData.user), ...userToPayload(userDeletionData.user),
@ -502,7 +503,7 @@ export class InternalHooks {
invitee_role: string; invitee_role: string;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.invited', eventName: 'n8n.audit.user.invited',
payload: { payload: {
...userToPayload(userInviteData.user), ...userToPayload(userInviteData.user),
@ -537,7 +538,7 @@ export class InternalHooks {
public_api: boolean; public_api: boolean;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.reinvited', eventName: 'n8n.audit.user.reinvited',
payload: { payload: {
...userToPayload(userReinviteData.user), ...userToPayload(userReinviteData.user),
@ -596,7 +597,7 @@ export class InternalHooks {
async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> { async onUserUpdate(userUpdateData: { user: User; fields_changed: string[] }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.updated', eventName: 'n8n.audit.user.updated',
payload: { payload: {
...userToPayload(userUpdateData.user), ...userToPayload(userUpdateData.user),
@ -615,7 +616,7 @@ export class InternalHooks {
invitee: User; invitee: User;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.invitation.accepted', eventName: 'n8n.audit.user.invitation.accepted',
payload: { payload: {
invitee: { invitee: {
@ -634,7 +635,7 @@ export class InternalHooks {
async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> { async onUserPasswordResetEmailClick(userPasswordResetData: { user: User }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.reset', eventName: 'n8n.audit.user.reset',
payload: { payload: {
...userToPayload(userPasswordResetData.user), ...userToPayload(userPasswordResetData.user),
@ -673,7 +674,7 @@ export class InternalHooks {
async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void> { async onApiKeyDeleted(apiKeyDeletedData: { user: User; public_api: boolean }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.api.deleted', eventName: 'n8n.audit.user.api.deleted',
payload: { payload: {
...userToPayload(apiKeyDeletedData.user), ...userToPayload(apiKeyDeletedData.user),
@ -688,7 +689,7 @@ export class InternalHooks {
async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise<void> { async onApiKeyCreated(apiKeyCreatedData: { user: User; public_api: boolean }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.api.created', eventName: 'n8n.audit.user.api.created',
payload: { payload: {
...userToPayload(apiKeyCreatedData.user), ...userToPayload(apiKeyCreatedData.user),
@ -703,7 +704,7 @@ export class InternalHooks {
async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> { async onUserPasswordResetRequestClick(userPasswordResetData: { user: User }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.reset.requested', eventName: 'n8n.audit.user.reset.requested',
payload: { payload: {
...userToPayload(userPasswordResetData.user), ...userToPayload(userPasswordResetData.user),
@ -727,7 +728,7 @@ export class InternalHooks {
}, },
): Promise<void> { ): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.signedup', eventName: 'n8n.audit.user.signedup',
payload: { payload: {
...userToPayload(user), ...userToPayload(user),
@ -751,7 +752,7 @@ export class InternalHooks {
public_api: boolean; public_api: boolean;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.email.failed', eventName: 'n8n.audit.user.email.failed',
payload: { payload: {
messageType: failedEmailData.message_type, messageType: failedEmailData.message_type,
@ -769,7 +770,7 @@ export class InternalHooks {
authenticationMethod: AuthenticationMethod; authenticationMethod: AuthenticationMethod;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.login.success', eventName: 'n8n.audit.user.login.success',
payload: { payload: {
authenticationMethod: userLoginData.authenticationMethod, authenticationMethod: userLoginData.authenticationMethod,
@ -785,7 +786,7 @@ export class InternalHooks {
reason?: string; reason?: string;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.login.failed', eventName: 'n8n.audit.user.login.failed',
payload: { payload: {
authenticationMethod: userLoginData.authenticationMethod, authenticationMethod: userLoginData.authenticationMethod,
@ -808,7 +809,7 @@ export class InternalHooks {
public_api: boolean; public_api: boolean;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.credentials.created', eventName: 'n8n.audit.user.credentials.created',
payload: { payload: {
...userToPayload(userCreatedCredentialsData.user), ...userToPayload(userCreatedCredentialsData.user),
@ -836,7 +837,7 @@ export class InternalHooks {
sharees_removed: number | null; sharees_removed: number | null;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.credentials.shared', eventName: 'n8n.audit.user.credentials.shared',
payload: { payload: {
...userToPayload(userSharedCredentialsData.user), ...userToPayload(userSharedCredentialsData.user),
@ -876,7 +877,7 @@ export class InternalHooks {
failure_reason?: string; failure_reason?: string;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.package.installed', eventName: 'n8n.audit.package.installed',
payload: { payload: {
...userToPayload(installationData.user), ...userToPayload(installationData.user),
@ -914,7 +915,7 @@ export class InternalHooks {
package_author_email?: string; package_author_email?: string;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.package.updated', eventName: 'n8n.audit.package.updated',
payload: { payload: {
...userToPayload(updateData.user), ...userToPayload(updateData.user),
@ -947,7 +948,7 @@ export class InternalHooks {
package_author_email?: string; package_author_email?: string;
}): Promise<void> { }): Promise<void> {
void Promise.all([ void Promise.all([
eventBus.sendAuditEvent({ this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.package.deleted', eventName: 'n8n.audit.package.deleted',
payload: { payload: {
...userToPayload(deleteData.user), ...userToPayload(deleteData.user),

View file

@ -67,7 +67,7 @@ import { setupAuthMiddlewares } from './middlewares';
import { isLdapEnabled } from './Ldap/helpers'; import { isLdapEnabled } from './Ldap/helpers';
import { AbstractServer } from './AbstractServer'; import { AbstractServer } from './AbstractServer';
import { PostHogClient } from './posthog'; import { PostHogClient } from './posthog';
import { eventBus } from './eventbus'; import { MessageEventBus } from '@/eventbus';
import { InternalHooks } from './InternalHooks'; import { InternalHooks } from './InternalHooks';
import { License } from './License'; import { License } from './License';
import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlController } from './sso/saml/routes/saml.controller.ee';
@ -416,10 +416,8 @@ export class Server extends AbstractServer {
// ---------------------------------------- // ----------------------------------------
// EventBus Setup // EventBus Setup
// ---------------------------------------- // ----------------------------------------
const eventBus = Container.get(MessageEventBus);
if (!eventBus.isInitialized) {
await eventBus.initialize(); await eventBus.initialize();
}
if (this.endpointPresetCredentials !== '') { if (this.endpointPresetCredentials !== '') {
// POST endpoint to set preset credentials // POST endpoint to set preset credentials

View file

@ -28,6 +28,8 @@ import { fork } from 'child_process';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import config from '@/config'; import config from '@/config';
import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository';
import { MessageEventBus } from '@/eventbus';
import { ExecutionDataRecoveryService } from '@/eventbus/executionDataRecovery.service';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import type { import type {
IExecutionResponse, IExecutionResponse,
@ -125,18 +127,13 @@ export class WorkflowRunner {
// does contain those messages. // does contain those messages.
try { try {
// Search for messages for this executionId in event logs // Search for messages for this executionId in event logs
const { eventBus } = await import('./eventbus'); const eventBus = Container.get(MessageEventBus);
const eventLogMessages = await eventBus.getEventsByExecutionId(executionId); const eventLogMessages = await eventBus.getEventsByExecutionId(executionId);
// Attempt to recover more better runData from these messages (but don't update the execution db entry yet) // Attempt to recover more better runData from these messages (but don't update the execution db entry yet)
if (eventLogMessages.length > 0) { if (eventLogMessages.length > 0) {
const { recoverExecutionDataFromEventLogMessages } = await import( const eventLogExecutionData = await Container.get(
'./eventbus/MessageEventBus/recoverEvents' ExecutionDataRecoveryService,
); ).recoverExecutionData(executionId, eventLogMessages, false);
const eventLogExecutionData = await recoverExecutionDataFromEventLogMessages(
executionId,
eventLogMessages,
false,
);
if (eventLogExecutionData) { if (eventLogExecutionData) {
fullRunData.data.resultData.runData = eventLogExecutionData.resultData.runData; fullRunData.data.resultData.runData = eventLogExecutionData.resultData.runData;
fullRunData.status = 'crashed'; fullRunData.status = 'crashed';

View file

@ -16,7 +16,7 @@ import { ActiveExecutions } from '@/ActiveExecutions';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { Server } from '@/Server'; import { Server } from '@/Server';
import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants'; import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants';
import { eventBus } from '@/eventbus'; import { MessageEventBus } from '@/eventbus';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
@ -127,7 +127,7 @@ export class Start extends BaseCommand {
} }
// Finally shut down Event Bus // Finally shut down Event Bus
await eventBus.close(); await Container.get(MessageEventBus).close();
} catch (error) { } catch (error) {
await this.exitWithCrash('There was an error shutting down n8n.', error); await this.exitWithCrash('There was an error shutting down n8n.', error);
} }

View file

@ -29,7 +29,7 @@ import { OwnershipService } from '@/services/ownership.service';
import type { ICredentialsOverwrite } from '@/Interfaces'; import type { ICredentialsOverwrite } from '@/Interfaces';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { rawBodyReader, bodyParser } from '@/middlewares'; import { rawBodyReader, bodyParser } from '@/middlewares';
import { eventBus } from '@/eventbus'; import { MessageEventBus } from '@/eventbus';
import type { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber'; import type { RedisServicePubSubSubscriber } from '@/services/redis/RedisServicePubSubSubscriber';
import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric';
import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service'; import { OrchestrationHandlerWorkerService } from '@/services/orchestration/worker/orchestration.handler.worker.service';
@ -307,7 +307,7 @@ export class Worker extends BaseCommand {
} }
async initEventBus() { async initEventBus() {
await eventBus.initialize({ await Container.get(MessageEventBus).initialize({
workerId: this.queueModeId, workerId: this.queueModeId,
}); });
} }

View file

@ -4,7 +4,7 @@ import config from '@/config';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import { MessageEventBus } from '@/eventbus';
import { License } from '@/License'; import { License } from '@/License';
import { LICENSE_FEATURES, inE2ETests } from '@/constants'; import { LICENSE_FEATURES, inE2ETests } from '@/constants';
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators'; import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
@ -91,6 +91,7 @@ export class E2EController {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly push: Push, private readonly push: Push,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
private readonly eventBus: MessageEventBus,
) { ) {
license.isFeatureEnabled = (feature: BooleanLicenseFeature) => license.isFeatureEnabled = (feature: BooleanLicenseFeature) =>
this.enabledFeatures[feature] ?? false; this.enabledFeatures[feature] ?? false;
@ -136,8 +137,8 @@ export class E2EController {
} }
private async resetLogStreaming() { private async resetLogStreaming() {
for (const id in eventBus.destinations) { for (const id in this.eventBus.destinations) {
await eventBus.removeDestination(id, false); await this.eventBus.removeDestination(id, false);
} }
} }

View file

@ -1,7 +1,18 @@
import { jsonParse } from 'n8n-workflow'; import { Service } from 'typedi';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import type { DeleteResult } from 'typeorm'; import type { DeleteResult } from 'typeorm';
import { In } from 'typeorm'; import { In } from 'typeorm';
import EventEmitter from 'events';
import uniqby from 'lodash/uniqBy';
import { jsonParse } from 'n8n-workflow';
import type { MessageEventBusDestinationOptions } from 'n8n-workflow';
import config from '@/config';
import { EventDestinationsRepository } from '@db/repositories/eventDestinations.repository';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { OrchestrationService } from '@/services/orchestration.service';
import { Logger } from '@/Logger';
import type { import type {
EventMessageTypes, EventMessageTypes,
EventNamesTypes, EventNamesTypes,
@ -9,10 +20,7 @@ import type {
} from '../EventMessageClasses/'; } from '../EventMessageClasses/';
import type { MessageEventBusDestination } from '../MessageEventBusDestination/MessageEventBusDestination.ee'; import type { MessageEventBusDestination } from '../MessageEventBusDestination/MessageEventBusDestination.ee';
import { MessageEventBusLogWriter } from '../MessageEventBusWriter/MessageEventBusLogWriter'; import { MessageEventBusLogWriter } from '../MessageEventBusWriter/MessageEventBusLogWriter';
import EventEmitter from 'events';
import config from '@/config';
import { messageEventBusDestinationFromDb } from '../MessageEventBusDestination/MessageEventBusDestinationFromDb'; import { messageEventBusDestinationFromDb } from '../MessageEventBusDestination/MessageEventBusDestinationFromDb';
import uniqby from 'lodash/uniqBy';
import type { EventMessageConfirmSource } from '../EventMessageClasses/EventMessageConfirm'; import type { EventMessageConfirmSource } from '../EventMessageClasses/EventMessageConfirm';
import type { EventMessageAuditOptions } from '../EventMessageClasses/EventMessageAudit'; import type { EventMessageAuditOptions } from '../EventMessageClasses/EventMessageAudit';
import { EventMessageAudit } from '../EventMessageClasses/EventMessageAudit'; import { EventMessageAudit } from '../EventMessageClasses/EventMessageAudit';
@ -25,16 +33,10 @@ import {
EventMessageGeneric, EventMessageGeneric,
eventMessageGenericDestinationTestEvent, eventMessageGenericDestinationTestEvent,
} from '../EventMessageClasses/EventMessageGeneric'; } from '../EventMessageClasses/EventMessageGeneric';
import { recoverExecutionDataFromEventLogMessages } from './recoverEvents';
import { METRICS_EVENT_NAME } from '../MessageEventBusDestination/Helpers.ee'; import { METRICS_EVENT_NAME } from '../MessageEventBusDestination/Helpers.ee';
import { Container, Service } from 'typedi';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions'; import type { AbstractEventMessageOptions } from '../EventMessageClasses/AbstractEventMessageOptions';
import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers'; import { getEventMessageObjectByType } from '../EventMessageClasses/Helpers';
import { OrchestrationService } from '@/services/orchestration.service'; import { ExecutionDataRecoveryService } from '../executionDataRecovery.service';
import { Logger } from '@/Logger';
import { EventDestinationsRepository } from '@db/repositories/eventDestinations.repository';
export type EventMessageReturnMode = 'sent' | 'unsent' | 'all' | 'unfinished'; export type EventMessageReturnMode = 'sent' | 'unsent' | 'all' | 'unfinished';
@ -50,7 +52,7 @@ export interface MessageEventBusInitializeOptions {
@Service() @Service()
export class MessageEventBus extends EventEmitter { export class MessageEventBus extends EventEmitter {
isInitialized: boolean; private isInitialized = false;
logWriter: MessageEventBusLogWriter; logWriter: MessageEventBusLogWriter;
@ -60,9 +62,15 @@ export class MessageEventBus extends EventEmitter {
private pushIntervalTimer: NodeJS.Timer; private pushIntervalTimer: NodeJS.Timer;
constructor(private readonly logger: Logger) { constructor(
private readonly logger: Logger,
private readonly executionRepository: ExecutionRepository,
private readonly eventDestinationsRepository: EventDestinationsRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly orchestrationService: OrchestrationService,
private readonly recoveryService: ExecutionDataRecoveryService,
) {
super(); super();
this.isInitialized = false;
} }
/** /**
@ -80,7 +88,7 @@ export class MessageEventBus extends EventEmitter {
this.logger.debug('Initializing event bus...'); this.logger.debug('Initializing event bus...');
const savedEventDestinations = await Container.get(EventDestinationsRepository).find({}); const savedEventDestinations = await this.eventDestinationsRepository.find({});
if (savedEventDestinations.length > 0) { if (savedEventDestinations.length > 0) {
for (const destinationData of savedEventDestinations) { for (const destinationData of savedEventDestinations) {
try { try {
@ -132,7 +140,7 @@ export class MessageEventBus extends EventEmitter {
// crashing, so we can't just mark them as crashed // crashing, so we can't just mark them as crashed
if (config.get('executions.mode') !== 'queue') { if (config.get('executions.mode') !== 'queue') {
const dbUnfinishedExecutionIds = ( const dbUnfinishedExecutionIds = (
await Container.get(ExecutionRepository).find({ await this.executionRepository.find({
where: { where: {
status: In(['running', 'new', 'unknown']), status: In(['running', 'new', 'unknown']),
}, },
@ -147,7 +155,7 @@ export class MessageEventBus extends EventEmitter {
if (unfinishedExecutionIds.length > 0) { if (unfinishedExecutionIds.length > 0) {
this.logger.warn(`Found unfinished executions: ${unfinishedExecutionIds.join(', ')}`); this.logger.warn(`Found unfinished executions: ${unfinishedExecutionIds.join(', ')}`);
this.logger.info('This could be due to a crash of an active workflow or a restart of n8n.'); this.logger.info('This could be due to a crash of an active workflow or a restart of n8n.');
const activeWorkflows = await Container.get(WorkflowRepository).find({ const activeWorkflows = await this.workflowRepository.find({
where: { active: true }, where: { active: true },
select: ['id', 'name'], select: ['id', 'name'],
}); });
@ -159,7 +167,7 @@ export class MessageEventBus extends EventEmitter {
} }
const recoveryAlreadyAttempted = this.logWriter?.isRecoveryProcessRunning(); const recoveryAlreadyAttempted = this.logWriter?.isRecoveryProcessRunning();
if (recoveryAlreadyAttempted || config.getEnv('eventBus.crashRecoveryMode') === 'simple') { if (recoveryAlreadyAttempted || config.getEnv('eventBus.crashRecoveryMode') === 'simple') {
await Container.get(ExecutionRepository).markAsCrashed(unfinishedExecutionIds); await this.executionRepository.markAsCrashed(unfinishedExecutionIds);
// if we end up here, it means that the previous recovery process did not finish // if we end up here, it means that the previous recovery process did not finish
// a possible reason would be that recreating the workflow data itself caused e.g an OOM error // a possible reason would be that recreating the workflow data itself caused e.g an OOM error
// in that case, we do not want to retry the recovery process, but rather mark the executions as crashed // in that case, we do not want to retry the recovery process, but rather mark the executions as crashed
@ -174,9 +182,9 @@ export class MessageEventBus extends EventEmitter {
this.logger.debug( this.logger.debug(
`No event messages found, marking execution ${executionId} as 'crashed'`, `No event messages found, marking execution ${executionId} as 'crashed'`,
); );
await Container.get(ExecutionRepository).markAsCrashed([executionId]); await this.executionRepository.markAsCrashed([executionId]);
} else { } else {
await recoverExecutionDataFromEventLogMessages( await this.recoveryService.recoverExecutionData(
executionId, executionId,
unsentAndUnfinished.unfinishedExecutions[executionId], unsentAndUnfinished.unfinishedExecutions[executionId],
true, true,
@ -207,7 +215,7 @@ export class MessageEventBus extends EventEmitter {
this.destinations[destination.getId()] = destination; this.destinations[destination.getId()] = destination;
this.destinations[destination.getId()].startListening(); this.destinations[destination.getId()].startListening();
if (notifyWorkers) { if (notifyWorkers) {
await Container.get(OrchestrationService).publish('restartEventBus'); await this.orchestrationService.publish('restartEventBus');
} }
return destination; return destination;
} }
@ -233,7 +241,7 @@ export class MessageEventBus extends EventEmitter {
delete this.destinations[id]; delete this.destinations[id];
} }
if (notifyWorkers) { if (notifyWorkers) {
await Container.get(OrchestrationService).publish('restartEventBus'); await this.orchestrationService.publish('restartEventBus');
} }
return result; return result;
} }
@ -243,7 +251,7 @@ export class MessageEventBus extends EventEmitter {
if (eventData) { if (eventData) {
const eventMessage = getEventMessageObjectByType(eventData); const eventMessage = getEventMessageObjectByType(eventData);
if (eventMessage) { if (eventMessage) {
await Container.get(MessageEventBus).send(eventMessage); await this.send(eventMessage);
} }
} }
return eventData; return eventData;
@ -370,7 +378,7 @@ export class MessageEventBus extends EventEmitter {
.slice(-amount); .slice(-amount);
for (const execution of filteredExecutionIds) { for (const execution of filteredExecutionIds) {
const data = await recoverExecutionDataFromEventLogMessages( const data = await this.recoveryService.recoverExecutionData(
execution.executionId, execution.executionId,
queryResult, queryResult,
false, false,
@ -450,5 +458,3 @@ export class MessageEventBus extends EventEmitter {
await this.send(new EventMessageNode(options)); await this.send(new EventMessageNode(options));
} }
} }
export const eventBus = Container.get(MessageEventBus);

View file

@ -1,205 +0,0 @@
import type { IRun, IRunExecutionData, ITaskData } from 'n8n-workflow';
import { NodeOperationError, WorkflowOperationError } from 'n8n-workflow';
import type { EventMessageTypes, EventNamesTypes } from '../EventMessageClasses';
import type { DateTime } from 'luxon';
import { Push } from '@/push';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { getWorkflowHooksMain } from '@/WorkflowExecuteAdditionalData';
import { ExecutionRepository } from '@db/repositories/execution.repository';
export async function recoverExecutionDataFromEventLogMessages(
executionId: string,
messages: EventMessageTypes[],
applyToDb: boolean,
): Promise<IRunExecutionData | undefined> {
const executionEntry = await Container.get(ExecutionRepository).findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (executionEntry && messages) {
let executionData = executionEntry.data;
let workflowError: WorkflowOperationError | undefined;
if (!executionData) {
executionData = { resultData: { runData: {} } };
}
let nodeNames: string[] = [];
if (
executionData?.resultData?.runData &&
Object.keys(executionData.resultData.runData).length > 0
) {
} else {
if (!executionData.resultData) {
executionData.resultData = {
runData: {},
};
} else {
if (!executionData.resultData.runData) {
executionData.resultData.runData = {};
}
}
}
nodeNames = executionEntry.workflowData.nodes.map((n) => n.name);
let lastNodeRunTimestamp: DateTime | undefined = undefined;
for (const nodeName of nodeNames) {
const nodeByName = executionEntry?.workflowData.nodes.find((n) => n.name === nodeName);
if (!nodeByName) continue;
const nodeStartedMessage = messages.find(
(message) =>
message.eventName === 'n8n.node.started' && message.payload.nodeName === nodeName,
);
const nodeFinishedMessage = messages.find(
(message) =>
message.eventName === 'n8n.node.finished' && message.payload.nodeName === nodeName,
);
const executionTime =
nodeStartedMessage && nodeFinishedMessage
? nodeFinishedMessage.ts.diff(nodeStartedMessage.ts).toMillis()
: 0;
let taskData: ITaskData;
if (executionData.resultData.runData[nodeName]?.length > 0) {
taskData = executionData.resultData.runData[nodeName][0];
} else {
taskData = {
startTime: nodeStartedMessage ? nodeStartedMessage.ts.toUnixInteger() : 0,
executionTime,
source: [null],
executionStatus: 'unknown',
};
}
if (nodeStartedMessage && !nodeFinishedMessage) {
const nodeError = new NodeOperationError(
nodeByName,
'Node crashed, possible out-of-memory issue',
{
message: 'Execution stopped at this node',
description:
"n8n may have run out of memory while executing it. More context and tips on how to avoid this <a href='https://docs.n8n.io/flow-logic/error-handling/memory-errors' target='_blank'>in the docs</a>",
},
);
workflowError = new WorkflowOperationError(
'Workflow did not finish, possible out-of-memory issue',
);
taskData.error = nodeError;
taskData.executionStatus = 'crashed';
executionData.resultData.lastNodeExecuted = nodeName;
if (nodeStartedMessage) lastNodeRunTimestamp = nodeStartedMessage.ts;
} else if (nodeStartedMessage && nodeFinishedMessage) {
taskData.executionStatus = 'success';
if (taskData.data === undefined) {
taskData.data = {
main: [
[
{
json: {
isArtificialRecoveredEventItem: true,
},
pairedItem: undefined,
},
],
],
};
}
}
if (!executionData.resultData.runData[nodeName]) {
executionData.resultData.runData[nodeName] = [taskData];
}
}
if (!lastNodeRunTimestamp) {
const workflowEndedMessage = messages.find((message) =>
(
[
'n8n.workflow.success',
'n8n.workflow.crashed',
'n8n.workflow.failed',
] as EventNamesTypes[]
).includes(message.eventName),
);
if (workflowEndedMessage) {
lastNodeRunTimestamp = workflowEndedMessage.ts;
} else {
if (!workflowError) {
workflowError = new WorkflowOperationError(
'Workflow did not finish, possible out-of-memory issue',
);
}
const workflowStartedMessage = messages.find(
(message) => message.eventName === 'n8n.workflow.started',
);
if (workflowStartedMessage) {
lastNodeRunTimestamp = workflowStartedMessage.ts;
}
}
}
if (!executionData.resultData.error && workflowError) {
executionData.resultData.error = workflowError;
}
if (applyToDb) {
const newStatus = executionEntry.status === 'failed' ? 'failed' : 'crashed';
await Container.get(ExecutionRepository).updateExistingExecution(executionId, {
data: executionData,
status: newStatus,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
});
await Container.get(InternalHooks).onWorkflowPostExecute(
executionId,
executionEntry.workflowData,
{
data: executionData,
finished: false,
mode: executionEntry.mode,
waitTill: executionEntry.waitTill ?? undefined,
startedAt: executionEntry.startedAt,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
status: newStatus,
},
);
const iRunData: IRun = {
data: executionData,
finished: false,
mode: executionEntry.mode,
waitTill: executionEntry.waitTill ?? undefined,
startedAt: executionEntry.startedAt,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
status: newStatus,
};
const workflowHooks = getWorkflowHooksMain(
{
userId: '',
workflowData: executionEntry.workflowData,
executionMode: executionEntry.mode,
executionData,
runData: executionData.resultData.runData,
retryOf: executionEntry.retryOf,
},
executionId,
);
// execute workflowExecuteAfter hook to trigger error workflow
await workflowHooks.executeHookFunctions('workflowExecuteAfter', [iRunData]);
const push = Container.get(Push);
// wait for UI to be back up and send the execution data
push.once('editorUiConnected', function handleUiBackUp() {
// add a small timeout to make sure the UI is back up
setTimeout(() => {
push.broadcast('executionRecovered', { executionId });
}, 1000);
});
}
return executionData;
}
return;
}

View file

@ -1,5 +1,15 @@
import express from 'express'; import express from 'express';
import { eventBus } from './MessageEventBus/MessageEventBus'; import type {
MessageEventBusDestinationWebhookOptions,
MessageEventBusDestinationOptions,
} from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Delete, Authorized, RequireGlobalScope } from '@/decorators';
import { AuthenticatedRequest } from '@/requests';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { MessageEventBus } from './MessageEventBus/MessageEventBus';
import { import {
isMessageEventBusDestinationSentryOptions, isMessageEventBusDestinationSentryOptions,
MessageEventBusDestinationSentry, MessageEventBusDestinationSentry,
@ -9,16 +19,8 @@ import {
MessageEventBusDestinationSyslog, MessageEventBusDestinationSyslog,
} from './MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; } from './MessageEventBusDestination/MessageEventBusDestinationSyslog.ee';
import { MessageEventBusDestinationWebhook } from './MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; import { MessageEventBusDestinationWebhook } from './MessageEventBusDestination/MessageEventBusDestinationWebhook.ee';
import type {
MessageEventBusDestinationWebhookOptions,
MessageEventBusDestinationOptions,
} from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Delete, Authorized, RequireGlobalScope } from '@/decorators';
import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee'; import type { MessageEventBusDestination } from './MessageEventBusDestination/MessageEventBusDestination.ee';
import { AuthenticatedRequest } from '@/requests';
import { logStreamingLicensedMiddleware } from './middleware/logStreamingEnabled.middleware.ee'; import { logStreamingLicensedMiddleware } from './middleware/logStreamingEnabled.middleware.ee';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
// ---------------------------------------- // ----------------------------------------
// TypeGuards // TypeGuards
@ -53,6 +55,8 @@ const isMessageEventBusDestinationOptions = (
@Authorized() @Authorized()
@RestController('/eventbus') @RestController('/eventbus')
export class EventBusControllerEE { export class EventBusControllerEE {
constructor(private readonly eventBus: MessageEventBus) {}
// ---------------------------------------- // ----------------------------------------
// Destinations // Destinations
// ---------------------------------------- // ----------------------------------------
@ -61,9 +65,9 @@ export class EventBusControllerEE {
@RequireGlobalScope('eventBusDestination:list') @RequireGlobalScope('eventBusDestination:list')
async getDestination(req: express.Request): Promise<MessageEventBusDestinationOptions[]> { async getDestination(req: express.Request): Promise<MessageEventBusDestinationOptions[]> {
if (isWithIdString(req.query)) { if (isWithIdString(req.query)) {
return await eventBus.findDestination(req.query.id); return await this.eventBus.findDestination(req.query.id);
} else { } else {
return await eventBus.findDestination(); return await this.eventBus.findDestination();
} }
} }
@ -75,22 +79,22 @@ export class EventBusControllerEE {
switch (req.body.__type) { switch (req.body.__type) {
case MessageEventBusDestinationTypeNames.sentry: case MessageEventBusDestinationTypeNames.sentry:
if (isMessageEventBusDestinationSentryOptions(req.body)) { if (isMessageEventBusDestinationSentryOptions(req.body)) {
result = await eventBus.addDestination( result = await this.eventBus.addDestination(
new MessageEventBusDestinationSentry(eventBus, req.body), new MessageEventBusDestinationSentry(this.eventBus, req.body),
); );
} }
break; break;
case MessageEventBusDestinationTypeNames.webhook: case MessageEventBusDestinationTypeNames.webhook:
if (isMessageEventBusDestinationWebhookOptions(req.body)) { if (isMessageEventBusDestinationWebhookOptions(req.body)) {
result = await eventBus.addDestination( result = await this.eventBus.addDestination(
new MessageEventBusDestinationWebhook(eventBus, req.body), new MessageEventBusDestinationWebhook(this.eventBus, req.body),
); );
} }
break; break;
case MessageEventBusDestinationTypeNames.syslog: case MessageEventBusDestinationTypeNames.syslog:
if (isMessageEventBusDestinationSyslogOptions(req.body)) { if (isMessageEventBusDestinationSyslogOptions(req.body)) {
result = await eventBus.addDestination( result = await this.eventBus.addDestination(
new MessageEventBusDestinationSyslog(eventBus, req.body), new MessageEventBusDestinationSyslog(this.eventBus, req.body),
); );
} }
break; break;
@ -115,7 +119,7 @@ export class EventBusControllerEE {
@RequireGlobalScope('eventBusDestination:test') @RequireGlobalScope('eventBusDestination:test')
async sendTestMessage(req: express.Request): Promise<boolean> { async sendTestMessage(req: express.Request): Promise<boolean> {
if (isWithIdString(req.query)) { if (isWithIdString(req.query)) {
return await eventBus.testDestination(req.query.id); return await this.eventBus.testDestination(req.query.id);
} }
return false; return false;
} }
@ -124,7 +128,7 @@ export class EventBusControllerEE {
@RequireGlobalScope('eventBusDestination:delete') @RequireGlobalScope('eventBusDestination:delete')
async deleteDestination(req: AuthenticatedRequest) { async deleteDestination(req: AuthenticatedRequest) {
if (isWithIdString(req.query)) { if (isWithIdString(req.query)) {
return await eventBus.removeDestination(req.query.id); return await this.eventBus.removeDestination(req.query.id);
} else { } else {
throw new BadRequestError('Query is missing id'); throw new BadRequestError('Query is missing id');
} }

View file

@ -1,21 +1,23 @@
import express from 'express'; import express from 'express';
import type { IRunExecutionData } from 'n8n-workflow';
import { EventMessageTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Authorized, RequireGlobalScope } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage'; import { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage';
import { EventMessageGeneric } from './EventMessageClasses/EventMessageGeneric'; import { EventMessageGeneric } from './EventMessageClasses/EventMessageGeneric';
import type { EventMessageWorkflowOptions } from './EventMessageClasses/EventMessageWorkflow'; import type { EventMessageWorkflowOptions } from './EventMessageClasses/EventMessageWorkflow';
import { EventMessageWorkflow } from './EventMessageClasses/EventMessageWorkflow'; import { EventMessageWorkflow } from './EventMessageClasses/EventMessageWorkflow';
import type { EventMessageReturnMode } from './MessageEventBus/MessageEventBus'; import type { EventMessageReturnMode } from './MessageEventBus/MessageEventBus';
import { eventBus } from './MessageEventBus/MessageEventBus'; import { MessageEventBus } from './MessageEventBus/MessageEventBus';
import type { EventMessageTypes, FailedEventSummary } from './EventMessageClasses'; import type { EventMessageTypes, FailedEventSummary } from './EventMessageClasses';
import { eventNamesAll } from './EventMessageClasses'; import { eventNamesAll } from './EventMessageClasses';
import type { EventMessageAuditOptions } from './EventMessageClasses/EventMessageAudit'; import type { EventMessageAuditOptions } from './EventMessageClasses/EventMessageAudit';
import { EventMessageAudit } from './EventMessageClasses/EventMessageAudit'; import { EventMessageAudit } from './EventMessageClasses/EventMessageAudit';
import type { IRunExecutionData } from 'n8n-workflow';
import { EventMessageTypeNames } from 'n8n-workflow';
import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode'; import type { EventMessageNodeOptions } from './EventMessageClasses/EventMessageNode';
import { EventMessageNode } from './EventMessageClasses/EventMessageNode'; import { EventMessageNode } from './EventMessageClasses/EventMessageNode';
import { recoverExecutionDataFromEventLogMessages } from './MessageEventBus/recoverEvents'; import { ExecutionDataRecoveryService } from './executionDataRecovery.service';
import { RestController, Get, Post, Authorized, RequireGlobalScope } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
// ---------------------------------------- // ----------------------------------------
// TypeGuards // TypeGuards
@ -34,6 +36,11 @@ const isWithQueryString = (candidate: unknown): candidate is { query: string } =
@Authorized() @Authorized()
@RestController('/eventbus') @RestController('/eventbus')
export class EventBusController { export class EventBusController {
constructor(
private readonly eventBus: MessageEventBus,
private readonly recoveryService: ExecutionDataRecoveryService,
) {}
// ---------------------------------------- // ----------------------------------------
// Events // Events
// ---------------------------------------- // ----------------------------------------
@ -45,17 +52,17 @@ export class EventBusController {
if (isWithQueryString(req.query)) { if (isWithQueryString(req.query)) {
switch (req.query.query as EventMessageReturnMode) { switch (req.query.query as EventMessageReturnMode) {
case 'sent': case 'sent':
return await eventBus.getEventsSent(); return await this.eventBus.getEventsSent();
case 'unsent': case 'unsent':
return await eventBus.getEventsUnsent(); return await this.eventBus.getEventsUnsent();
case 'unfinished': case 'unfinished':
return await eventBus.getUnfinishedExecutions(); return await this.eventBus.getUnfinishedExecutions();
case 'all': case 'all':
default: default:
return await eventBus.getEventsAll(); return await this.eventBus.getEventsAll();
} }
} else { } else {
return await eventBus.getEventsAll(); return await this.eventBus.getEventsAll();
} }
} }
@ -63,7 +70,7 @@ export class EventBusController {
@RequireGlobalScope('eventBusEvent:list') @RequireGlobalScope('eventBusEvent:list')
async getFailedEvents(req: express.Request): Promise<FailedEventSummary[]> { async getFailedEvents(req: express.Request): Promise<FailedEventSummary[]> {
const amount = parseInt(req.query?.amount as string) ?? 5; const amount = parseInt(req.query?.amount as string) ?? 5;
return await eventBus.getEventsFailed(amount); return await this.eventBus.getEventsFailed(amount);
} }
@Get('/execution/:id') @Get('/execution/:id')
@ -74,7 +81,7 @@ export class EventBusController {
if (req.query?.logHistory) { if (req.query?.logHistory) {
logHistory = parseInt(req.query.logHistory as string, 10); logHistory = parseInt(req.query.logHistory as string, 10);
} }
return await eventBus.getEventsByExecutionId(req.params.id, logHistory); return await this.eventBus.getEventsByExecutionId(req.params.id, logHistory);
} }
return; return;
} }
@ -86,9 +93,9 @@ export class EventBusController {
if (req.params?.id) { if (req.params?.id) {
const logHistory = parseInt(req.query.logHistory as string, 10) || undefined; const logHistory = parseInt(req.query.logHistory as string, 10) || undefined;
const applyToDb = req.query.applyToDb !== undefined ? !!req.query.applyToDb : true; const applyToDb = req.query.applyToDb !== undefined ? !!req.query.applyToDb : true;
const messages = await eventBus.getEventsByExecutionId(id, logHistory); const messages = await this.eventBus.getEventsByExecutionId(id, logHistory);
if (messages.length > 0) { if (messages.length > 0) {
return await recoverExecutionDataFromEventLogMessages(id, messages, applyToDb); return await this.recoveryService.recoverExecutionData(id, messages, applyToDb);
} }
} }
return; return;
@ -113,7 +120,7 @@ export class EventBusController {
default: default:
msg = new EventMessageGeneric(req.body); msg = new EventMessageGeneric(req.body);
} }
await eventBus.send(msg); await this.eventBus.send(msg);
} else { } else {
throw new BadRequestError( throw new BadRequestError(
'Body is not a serialized EventMessage or eventName does not match format {namespace}.{domain}.{event}', 'Body is not a serialized EventMessage or eventName does not match format {namespace}.{domain}.{event}',

View file

@ -0,0 +1,212 @@
import { Container, Service } from 'typedi';
import type { DateTime } from 'luxon';
import { Push } from '@/push';
import { InternalHooks } from '@/InternalHooks';
import type { IRun, IRunExecutionData, ITaskData } from 'n8n-workflow';
import { NodeOperationError, WorkflowOperationError, sleep } from 'n8n-workflow';
import { ExecutionRepository } from '@db/repositories/execution.repository';
import { getWorkflowHooksMain } from '@/WorkflowExecuteAdditionalData';
import type { EventMessageTypes, EventNamesTypes } from './EventMessageClasses';
@Service()
export class ExecutionDataRecoveryService {
constructor(
private readonly push: Push,
private readonly executionRepository: ExecutionRepository,
) {}
async recoverExecutionData(
executionId: string,
messages: EventMessageTypes[],
applyToDb: boolean,
): Promise<IRunExecutionData | undefined> {
const executionEntry = await this.executionRepository.findSingleExecution(executionId, {
includeData: true,
unflattenData: true,
});
if (executionEntry && messages) {
let executionData = executionEntry.data;
let workflowError: WorkflowOperationError | undefined;
if (!executionData) {
executionData = { resultData: { runData: {} } };
}
let nodeNames: string[] = [];
if (
executionData?.resultData?.runData &&
Object.keys(executionData.resultData.runData).length > 0
) {
} else {
if (!executionData.resultData) {
executionData.resultData = {
runData: {},
};
} else {
if (!executionData.resultData.runData) {
executionData.resultData.runData = {};
}
}
}
nodeNames = executionEntry.workflowData.nodes.map((n) => n.name);
let lastNodeRunTimestamp: DateTime | undefined = undefined;
for (const nodeName of nodeNames) {
const nodeByName = executionEntry?.workflowData.nodes.find((n) => n.name === nodeName);
if (!nodeByName) continue;
const nodeStartedMessage = messages.find(
(message) =>
message.eventName === 'n8n.node.started' && message.payload.nodeName === nodeName,
);
const nodeFinishedMessage = messages.find(
(message) =>
message.eventName === 'n8n.node.finished' && message.payload.nodeName === nodeName,
);
const executionTime =
nodeStartedMessage && nodeFinishedMessage
? nodeFinishedMessage.ts.diff(nodeStartedMessage.ts).toMillis()
: 0;
let taskData: ITaskData;
if (executionData.resultData.runData[nodeName]?.length > 0) {
taskData = executionData.resultData.runData[nodeName][0];
} else {
taskData = {
startTime: nodeStartedMessage ? nodeStartedMessage.ts.toUnixInteger() : 0,
executionTime,
source: [null],
executionStatus: 'unknown',
};
}
if (nodeStartedMessage && !nodeFinishedMessage) {
const nodeError = new NodeOperationError(
nodeByName,
'Node crashed, possible out-of-memory issue',
{
message: 'Execution stopped at this node',
description:
"n8n may have run out of memory while executing it. More context and tips on how to avoid this <a href='https://docs.n8n.io/flow-logic/error-handling/memory-errors' target='_blank'>in the docs</a>",
},
);
workflowError = new WorkflowOperationError(
'Workflow did not finish, possible out-of-memory issue',
);
taskData.error = nodeError;
taskData.executionStatus = 'crashed';
executionData.resultData.lastNodeExecuted = nodeName;
if (nodeStartedMessage) lastNodeRunTimestamp = nodeStartedMessage.ts;
} else if (nodeStartedMessage && nodeFinishedMessage) {
taskData.executionStatus = 'success';
if (taskData.data === undefined) {
taskData.data = {
main: [
[
{
json: {
isArtificialRecoveredEventItem: true,
},
pairedItem: undefined,
},
],
],
};
}
}
if (!executionData.resultData.runData[nodeName]) {
executionData.resultData.runData[nodeName] = [taskData];
}
}
if (!lastNodeRunTimestamp) {
const workflowEndedMessage = messages.find((message) =>
(
[
'n8n.workflow.success',
'n8n.workflow.crashed',
'n8n.workflow.failed',
] as EventNamesTypes[]
).includes(message.eventName),
);
if (workflowEndedMessage) {
lastNodeRunTimestamp = workflowEndedMessage.ts;
} else {
if (!workflowError) {
workflowError = new WorkflowOperationError(
'Workflow did not finish, possible out-of-memory issue',
);
}
const workflowStartedMessage = messages.find(
(message) => message.eventName === 'n8n.workflow.started',
);
if (workflowStartedMessage) {
lastNodeRunTimestamp = workflowStartedMessage.ts;
}
}
}
if (!executionData.resultData.error && workflowError) {
executionData.resultData.error = workflowError;
}
if (applyToDb) {
const newStatus = executionEntry.status === 'failed' ? 'failed' : 'crashed';
await this.executionRepository.updateExistingExecution(executionId, {
data: executionData,
status: newStatus,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
});
await Container.get(InternalHooks).onWorkflowPostExecute(
executionId,
executionEntry.workflowData,
{
data: executionData,
finished: false,
mode: executionEntry.mode,
waitTill: executionEntry.waitTill ?? undefined,
startedAt: executionEntry.startedAt,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
status: newStatus,
},
);
const iRunData: IRun = {
data: executionData,
finished: false,
mode: executionEntry.mode,
waitTill: executionEntry.waitTill ?? undefined,
startedAt: executionEntry.startedAt,
stoppedAt: lastNodeRunTimestamp?.toJSDate(),
status: newStatus,
};
const workflowHooks = getWorkflowHooksMain(
{
userId: '',
workflowData: executionEntry.workflowData,
executionMode: executionEntry.mode,
executionData,
runData: executionData.resultData.runData,
retryOf: executionEntry.retryOf,
},
executionId,
);
// execute workflowExecuteAfter hook to trigger error workflow
await workflowHooks.executeHookFunctions('workflowExecuteAfter', [iRunData]);
// wait for UI to be back up and send the execution data
this.push.once('editorUiConnected', async () => {
// add a small timeout to make sure the UI is back up
await sleep(1000);
this.push.broadcast('executionRecovered', { executionId });
});
}
return executionData;
}
return;
}
}

View file

@ -1 +1,4 @@
export { eventBus } from './MessageEventBus/MessageEventBus'; export { MessageEventBus } from './MessageEventBus/MessageEventBus';
export { EventMessageTypes } from './EventMessageClasses';
export { EventPayloadWorkflow } from './EventMessageClasses/EventMessageWorkflow';
export { METRICS_EVENT_NAME, getLabelsForEvent } from './MessageEventBusDestination/Helpers.ee';

View file

@ -8,12 +8,12 @@ import { Service } from 'typedi';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { CacheService } from '@/services/cache/cache.service'; import { CacheService } from '@/services/cache/cache.service';
import type { EventMessageTypes } from '@/eventbus/EventMessageClasses';
import { import {
MessageEventBus,
METRICS_EVENT_NAME, METRICS_EVENT_NAME,
getLabelsForEvent, getLabelsForEvent,
} from '@/eventbus/MessageEventBusDestination/Helpers.ee'; type EventMessageTypes,
import { eventBus } from '@/eventbus'; } from '@/eventbus';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
@Service() @Service()
@ -21,6 +21,7 @@ export class MetricsService extends EventEmitter {
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly eventBus: MessageEventBus,
) { ) {
super(); super();
} }
@ -151,7 +152,7 @@ export class MetricsService extends EventEmitter {
if (!config.getEnv('endpoints.metrics.includeMessageEventBusMetrics')) { if (!config.getEnv('endpoints.metrics.includeMessageEventBusMetrics')) {
return; return;
} }
eventBus.on(METRICS_EVENT_NAME, (event: EventMessageTypes) => { this.eventBus.on(METRICS_EVENT_NAME, (event: EventMessageTypes) => {
const counter = this.getCounterForEvent(event); const counter = this.getCounterForEvent(event);
if (!counter) return; if (!counter) return;
counter.inc(1); counter.inc(1);

View file

@ -1,9 +1,9 @@
import { Container } from 'typedi';
import config from '@/config'; import config from '@/config';
import axios from 'axios'; import axios from 'axios';
import syslog from 'syslog-client'; import syslog from 'syslog-client';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import type { User } from '@db/entities/User';
import type { import type {
MessageEventBusDestinationSentryOptions, MessageEventBusDestinationSentryOptions,
MessageEventBusDestinationSyslogOptions, MessageEventBusDestinationSyslogOptions,
@ -14,7 +14,9 @@ import {
defaultMessageEventBusDestinationSyslogOptions, defaultMessageEventBusDestinationSyslogOptions,
defaultMessageEventBusDestinationWebhookOptions, defaultMessageEventBusDestinationWebhookOptions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { eventBus } from '@/eventbus';
import type { User } from '@db/entities/User';
import { MessageEventBus } from '@/eventbus';
import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric'; import { EventMessageGeneric } from '@/eventbus/EventMessageClasses/EventMessageGeneric';
import type { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee'; import type { MessageEventBusDestinationSyslog } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationSyslog.ee';
import type { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee'; import type { MessageEventBusDestinationWebhook } from '@/eventbus/MessageEventBusDestination/MessageEventBusDestinationWebhook.ee';
@ -23,9 +25,11 @@ import { EventMessageAudit } from '@/eventbus/EventMessageClasses/EventMessageAu
import type { EventNamesTypes } from '@/eventbus/EventMessageClasses'; import type { EventNamesTypes } from '@/eventbus/EventMessageClasses';
import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow'; import { EventMessageWorkflow } from '@/eventbus/EventMessageClasses/EventMessageWorkflow';
import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode'; import { EventMessageNode } from '@/eventbus/EventMessageClasses/EventMessageNode';
import { ExecutionDataRecoveryService } from '@/eventbus/executionDataRecovery.service';
import * as utils from './shared/utils'; import * as utils from './shared/utils';
import { createUser } from './shared/db/users'; import { createUser } from './shared/db/users';
import { mockInstance } from '../shared/mocking';
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
jest.mock('axios'); jest.mock('axios');
@ -64,6 +68,8 @@ const testSentryDestination: MessageEventBusDestinationSentryOptions = {
subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'], subscribedEvents: ['n8n.test.message', 'n8n.audit.user.updated'],
}; };
let eventBus: MessageEventBus;
async function confirmIdInAll(id: string) { async function confirmIdInAll(id: string) {
const sent = await eventBus.getEventsAll(); const sent = await eventBus.getEventsAll();
expect(sent.length).toBeGreaterThan(0); expect(sent.length).toBeGreaterThan(0);
@ -76,6 +82,7 @@ async function confirmIdSent(id: string) {
expect(sent.find((msg) => msg.id === id)).toBeTruthy(); expect(sent.find((msg) => msg.id === id)).toBeTruthy();
} }
mockInstance(ExecutionDataRecoveryService);
const testServer = utils.setupTestServer({ const testServer = utils.setupTestServer({
endpointGroups: ['eventBus'], endpointGroups: ['eventBus'],
enabledFeatures: ['feat:logStreaming'], enabledFeatures: ['feat:logStreaming'],
@ -90,12 +97,13 @@ beforeAll(async () => {
config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter'); config.set('eventBus.logWriter.logBaseName', 'n8n-test-logwriter');
config.set('eventBus.logWriter.keepLogCount', 1); config.set('eventBus.logWriter.keepLogCount', 1);
await eventBus.initialize({}); eventBus = Container.get(MessageEventBus);
await eventBus.initialize();
}); });
afterAll(async () => { afterAll(async () => {
jest.mock('@/eventbus/MessageEventBus/MessageEventBus'); jest.mock('@/eventbus/MessageEventBus/MessageEventBus');
await eventBus.close(); await eventBus?.close();
}); });
test('should have a running logwriter process', () => { test('should have a running logwriter process', () => {

View file

@ -1,7 +1,12 @@
import type { SuperAgentTest } from 'supertest'; import type { SuperAgentTest } from 'supertest';
import * as utils from './shared/utils/';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { MessageEventBus } from '@/eventbus';
import { ExecutionDataRecoveryService } from '@/eventbus/executionDataRecovery.service';
import * as utils from './shared/utils/';
import { createUser } from './shared/db/users'; import { createUser } from './shared/db/users';
import { mockInstance } from '../shared/mocking';
/** /**
* NOTE: due to issues with mocking the MessageEventBus in multiple tests running in parallel, * NOTE: due to issues with mocking the MessageEventBus in multiple tests running in parallel,
@ -12,6 +17,8 @@ import { createUser } from './shared/db/users';
let owner: User; let owner: User;
let authOwnerAgent: SuperAgentTest; let authOwnerAgent: SuperAgentTest;
mockInstance(MessageEventBus);
mockInstance(ExecutionDataRecoveryService);
const testServer = utils.setupTestServer({ const testServer = utils.setupTestServer({
endpointGroups: ['eventBus'], endpointGroups: ['eventBus'],
enabledFeatures: [], // do not enable logstreaming enabledFeatures: [], // do not enable logstreaming

View file

@ -1,11 +1,16 @@
import { setupTestServer } from './shared/utils'; import { Container } from 'typedi';
import config from '@/config';
import request from 'supertest';
import Container from 'typedi';
import { MetricsService } from '@/services/metrics.service';
import { N8N_VERSION } from '@/constants';
import { parse as semverParse } from 'semver'; import { parse as semverParse } from 'semver';
import request from 'supertest';
import config from '@/config';
import { N8N_VERSION } from '@/constants';
import { MetricsService } from '@/services/metrics.service';
import { ExecutionDataRecoveryService } from '@/eventbus/executionDataRecovery.service';
import { setupTestServer } from './shared/utils';
import { mockInstance } from '../shared/mocking';
mockInstance(ExecutionDataRecoveryService);
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
config.set('endpoints.metrics.enable', true); config.set('endpoints.metrics.enable', true);
config.set('endpoints.metrics.includeDefaultMetrics', false); config.set('endpoints.metrics.includeDefaultMetrics', false);

View file

@ -3,6 +3,7 @@ import { mock } from 'jest-mock-extended';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { MessageEventBus } from '@/eventbus';
import { Telemetry } from '@/telemetry'; import { Telemetry } from '@/telemetry';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import { WorkflowService } from '@/workflows/workflow.service'; import { WorkflowService } from '@/workflows/workflow.service';
@ -13,16 +14,14 @@ import { createOwner } from '../shared/db/users';
import { createWorkflow } from '../shared/db/workflows'; import { createWorkflow } from '../shared/db/workflows';
let workflowService: WorkflowService; let workflowService: WorkflowService;
let activeWorkflowRunner: ActiveWorkflowRunner; const activeWorkflowRunner = mockInstance(ActiveWorkflowRunner);
let orchestrationService: OrchestrationService; const orchestrationService = mockInstance(OrchestrationService);
mockInstance(MessageEventBus);
mockInstance(Telemetry);
beforeAll(async () => { beforeAll(async () => {
await testDb.init(); await testDb.init();
activeWorkflowRunner = mockInstance(ActiveWorkflowRunner);
orchestrationService = mockInstance(OrchestrationService);
mockInstance(Telemetry);
workflowService = new WorkflowService( workflowService = new WorkflowService(
mock(), mock(),
mock(), mock(),

View file

@ -12,7 +12,7 @@ let telemetry: Telemetry;
describe('InternalHooks', () => { describe('InternalHooks', () => {
beforeAll(() => { beforeAll(() => {
telemetry = mockInstance(Telemetry); telemetry = mockInstance(Telemetry);
internalHooks = new InternalHooks(telemetry, mock(), mock(), mock(), mock()); internalHooks = new InternalHooks(telemetry, mock(), mock(), mock(), mock(), mock());
}); });
it('Should be defined', () => { it('Should be defined', () => {

View file

@ -2,7 +2,7 @@ import Container from 'typedi';
import config from '@/config'; import config from '@/config';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import type { RedisServiceWorkerResponseObject } from '@/services/redis/RedisServiceCommands'; import type { RedisServiceWorkerResponseObject } from '@/services/redis/RedisServiceCommands';
import { eventBus } from '@/eventbus'; import { MessageEventBus } from '@/eventbus';
import { RedisService } from '@/services/redis.service'; import { RedisService } from '@/services/redis.service';
import { handleWorkerResponseMessageMain } from '@/services/orchestration/main/handleWorkerResponseMessageMain'; import { handleWorkerResponseMessageMain } from '@/services/orchestration/main/handleWorkerResponseMessageMain';
import { handleCommandMessageMain } from '@/services/orchestration/main/handleCommandMessageMain'; import { handleCommandMessageMain } from '@/services/orchestration/main/handleCommandMessageMain';
@ -37,9 +37,11 @@ const workerRestartEventbusResponse: RedisServiceWorkerResponseObject = {
describe('Orchestration Service', () => { describe('Orchestration Service', () => {
const logger = mockInstance(Logger); const logger = mockInstance(Logger);
mockInstance(Push); mockInstance(Push);
beforeAll(async () => {
mockInstance(RedisService); mockInstance(RedisService);
mockInstance(ExternalSecretsManager); mockInstance(ExternalSecretsManager);
const eventBus = mockInstance(MessageEventBus);
beforeAll(async () => {
jest.mock('ioredis', () => { jest.mock('ioredis', () => {
const Redis = require('ioredis-mock'); const Redis = require('ioredis-mock');
if (typeof Redis === 'object') { if (typeof Redis === 'object') {
@ -110,8 +112,7 @@ describe('Orchestration Service', () => {
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
}); });
test('should reject command messages from iteslf', async () => { test('should reject command messages from itself', async () => {
jest.spyOn(eventBus, 'restart');
const response = await handleCommandMessageMain( const response = await handleCommandMessageMain(
JSON.stringify({ ...workerRestartEventbusResponse, senderId: queueModeId }), JSON.stringify({ ...workerRestartEventbusResponse, senderId: queueModeId }),
); );
@ -119,7 +120,6 @@ describe('Orchestration Service', () => {
expect(response!.command).toEqual('restartEventBus'); expect(response!.command).toEqual('restartEventBus');
expect(response!.senderId).toEqual(queueModeId); expect(response!.senderId).toEqual(queueModeId);
expect(eventBus.restart).not.toHaveBeenCalled(); expect(eventBus.restart).not.toHaveBeenCalled();
jest.spyOn(eventBus, 'restart').mockRestore();
}); });
test('should send command messages', async () => { test('should send command messages', async () => {