mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
refactor(core): Add shutdown method to modules (#17322)
This commit is contained in:
@@ -113,6 +113,22 @@ export class ModuleRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async shutdownModule(moduleName: ModuleName) {
|
||||||
|
const moduleEntry = this.moduleMetadata.get(moduleName);
|
||||||
|
|
||||||
|
if (!moduleEntry) {
|
||||||
|
this.logger.debug('Skipping shutdown for unregistered module', { moduleName });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Container.get(moduleEntry.class).shutdown?.();
|
||||||
|
|
||||||
|
const index = this.activeModules.indexOf(moduleName);
|
||||||
|
if (index > -1) this.activeModules.splice(index, 1);
|
||||||
|
|
||||||
|
this.logger.debug(`Shut down module "${moduleName}"`);
|
||||||
|
}
|
||||||
|
|
||||||
isActive(moduleName: ModuleName) {
|
isActive(moduleName: ModuleName) {
|
||||||
return this.activeModules.includes(moduleName);
|
return this.activeModules.includes(moduleName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export class ModuleMetadata {
|
|||||||
this.modules.set(moduleName, moduleEntry);
|
this.modules.set(moduleName, moduleEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get(moduleName: string) {
|
||||||
|
return this.modules.get(moduleName);
|
||||||
|
}
|
||||||
|
|
||||||
getEntries() {
|
getEntries() {
|
||||||
return [...this.modules.entries()];
|
return [...this.modules.entries()];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type ModuleSettings = Record<string, unknown>;
|
|||||||
|
|
||||||
export interface ModuleInterface {
|
export interface ModuleInterface {
|
||||||
init?(): Promise<void>;
|
init?(): Promise<void>;
|
||||||
|
shutdown?(): Promise<void>;
|
||||||
entities?(): Promise<EntityClass[]>;
|
entities?(): Promise<EntityClass[]>;
|
||||||
settings?(): Promise<ModuleSettings>;
|
settings?(): Promise<ModuleSettings>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
|
|
||||||
import { OnShutdown } from '../on-shutdown';
|
import { OnShutdown } from '../on-shutdown';
|
||||||
import { ShutdownRegistryMetadata } from '../shutdown-registry-metadata';
|
import { ShutdownMetadata } from '../shutdown-metadata';
|
||||||
|
|
||||||
describe('OnShutdown', () => {
|
describe('OnShutdown', () => {
|
||||||
let shutdownRegistryMetadata: ShutdownRegistryMetadata;
|
let shutdownMetadata: ShutdownMetadata;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
shutdownRegistryMetadata = new ShutdownRegistryMetadata();
|
shutdownMetadata = new ShutdownMetadata();
|
||||||
Container.set(ShutdownRegistryMetadata, shutdownRegistryMetadata);
|
Container.set(ShutdownMetadata, shutdownMetadata);
|
||||||
jest.spyOn(shutdownRegistryMetadata, 'register');
|
jest.spyOn(shutdownMetadata, 'register');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should register a methods that is decorated with OnShutdown', () => {
|
it('should register a methods that is decorated with OnShutdown', () => {
|
||||||
@@ -19,8 +19,8 @@ describe('OnShutdown', () => {
|
|||||||
async onShutdown() {}
|
async onShutdown() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(1);
|
expect(shutdownMetadata.register).toHaveBeenCalledTimes(1);
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||||
methodName: 'onShutdown',
|
methodName: 'onShutdown',
|
||||||
serviceClass: TestClass,
|
serviceClass: TestClass,
|
||||||
});
|
});
|
||||||
@@ -36,12 +36,12 @@ describe('OnShutdown', () => {
|
|||||||
async two() {}
|
async two() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(2);
|
expect(shutdownMetadata.register).toHaveBeenCalledTimes(2);
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||||
methodName: 'one',
|
methodName: 'one',
|
||||||
serviceClass: TestClass,
|
serviceClass: TestClass,
|
||||||
});
|
});
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||||
methodName: 'two',
|
methodName: 'two',
|
||||||
serviceClass: TestClass,
|
serviceClass: TestClass,
|
||||||
});
|
});
|
||||||
@@ -56,9 +56,9 @@ describe('OnShutdown', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(1);
|
expect(shutdownMetadata.register).toHaveBeenCalledTimes(1);
|
||||||
// @ts-expect-error We are checking internal parts of the shutdown service
|
// @ts-expect-error We are checking internal parts of the shutdown service
|
||||||
expect(shutdownRegistryMetadata.handlersByPriority[10].length).toEqual(1);
|
expect(shutdownMetadata.handlersByPriority[10].length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if the decorated member is not a function', () => {
|
it('should throw an error if the decorated member is not a function', () => {
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ export {
|
|||||||
DEFAULT_SHUTDOWN_PRIORITY,
|
DEFAULT_SHUTDOWN_PRIORITY,
|
||||||
LOWEST_SHUTDOWN_PRIORITY,
|
LOWEST_SHUTDOWN_PRIORITY,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
export { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
export { ShutdownMetadata } from './shutdown-metadata';
|
||||||
export { OnShutdown } from './on-shutdown';
|
export { OnShutdown } from './on-shutdown';
|
||||||
export type { ShutdownHandler, ShutdownServiceClass } from './types';
|
export type { ShutdownHandler, ShutdownServiceClass } from './types';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Container } from '@n8n/di';
|
|||||||
import { UnexpectedError } from 'n8n-workflow';
|
import { UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { DEFAULT_SHUTDOWN_PRIORITY } from './constants';
|
import { DEFAULT_SHUTDOWN_PRIORITY } from './constants';
|
||||||
import { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
import { ShutdownMetadata } from './shutdown-metadata';
|
||||||
import type { ShutdownServiceClass } from './types';
|
import type { ShutdownServiceClass } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +31,7 @@ export const OnShutdown =
|
|||||||
const methodName = String(propertyKey);
|
const methodName = String(propertyKey);
|
||||||
// TODO: assert that serviceClass is decorated with @Service
|
// TODO: assert that serviceClass is decorated with @Service
|
||||||
if (typeof descriptor?.value === 'function') {
|
if (typeof descriptor?.value === 'function') {
|
||||||
Container.get(ShutdownRegistryMetadata).register(priority, { serviceClass, methodName });
|
Container.get(ShutdownMetadata).register(priority, { serviceClass, methodName });
|
||||||
} else {
|
} else {
|
||||||
const name = `${serviceClass.name}.${methodName}()`;
|
const name = `${serviceClass.name}.${methodName}()`;
|
||||||
throw new UnexpectedError(
|
throw new UnexpectedError(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { HIGHEST_SHUTDOWN_PRIORITY, LOWEST_SHUTDOWN_PRIORITY } from './constants
|
|||||||
import type { ShutdownHandler } from './types';
|
import type { ShutdownHandler } from './types';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ShutdownRegistryMetadata {
|
export class ShutdownMetadata {
|
||||||
private handlersByPriority: ShutdownHandler[][] = [];
|
private handlersByPriority: ShutdownHandler[][] = [];
|
||||||
|
|
||||||
register(priority: number, handler: ShutdownHandler) {
|
register(priority: number, handler: ShutdownHandler) {
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import { SettingsRepository } from '@n8n/db';
|
import { SettingsRepository } from '@n8n/db';
|
||||||
import { OnPubSubEvent, OnShutdown } from '@n8n/decorators';
|
import { OnPubSubEvent } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { Cipher, type IExternalSecretsManager } from 'n8n-core';
|
import { Cipher, type IExternalSecretsManager } from 'n8n-core';
|
||||||
import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow';
|
import { jsonParse, type IDataObject, ensureError, UnexpectedError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { EventService } from '@/events/event.service';
|
|
||||||
import { License } from '@/license';
|
|
||||||
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EXTERNAL_SECRETS_DB_KEY,
|
EXTERNAL_SECRETS_DB_KEY,
|
||||||
EXTERNAL_SECRETS_INITIAL_BACKOFF,
|
EXTERNAL_SECRETS_INITIAL_BACKOFF,
|
||||||
@@ -18,6 +14,10 @@ import { ExternalSecretsProviders } from './external-secrets-providers.ee';
|
|||||||
import { ExternalSecretsConfig } from './external-secrets.config';
|
import { ExternalSecretsConfig } from './external-secrets.config';
|
||||||
import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types';
|
import type { ExternalSecretsSettings, SecretsProvider, SecretsProviderSettings } from './types';
|
||||||
|
|
||||||
|
import { EventService } from '@/events/event.service';
|
||||||
|
import { License } from '@/license';
|
||||||
|
import { Publisher } from '@/scaling/pubsub/publisher.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExternalSecretsManager implements IExternalSecretsManager {
|
export class ExternalSecretsManager implements IExternalSecretsManager {
|
||||||
private providers: Record<string, SecretsProvider> = {};
|
private providers: Record<string, SecretsProvider> = {};
|
||||||
@@ -65,7 +65,6 @@ export class ExternalSecretsManager implements IExternalSecretsManager {
|
|||||||
this.logger.debug('External secrets manager initialized');
|
this.logger.debug('External secrets manager initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnShutdown()
|
|
||||||
shutdown() {
|
shutdown() {
|
||||||
clearInterval(this.updateInterval);
|
clearInterval(this.updateInterval);
|
||||||
Object.values(this.providers).forEach((p) => {
|
Object.values(this.providers).forEach((p) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ModuleInterface } from '@n8n/decorators';
|
import type { ModuleInterface } from '@n8n/decorators';
|
||||||
import { BackendModule } from '@n8n/decorators';
|
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
|
|
||||||
@BackendModule({ name: 'external-secrets', licenseFlag: 'feat:externalSecrets' })
|
@BackendModule({ name: 'external-secrets', licenseFlag: 'feat:externalSecrets' })
|
||||||
@@ -16,4 +16,11 @@ export class ExternalSecretsModule implements ModuleInterface {
|
|||||||
await externalSecretsManager.init();
|
await externalSecretsManager.init();
|
||||||
externalSecretsProxy.setManager(externalSecretsManager);
|
externalSecretsProxy.setManager(externalSecretsManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnShutdown()
|
||||||
|
async shutdown() {
|
||||||
|
const { ExternalSecretsManager } = await import('./external-secrets-manager.ee');
|
||||||
|
|
||||||
|
Container.get(ExternalSecretsManager).shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ModuleInterface } from '@n8n/decorators';
|
import type { ModuleInterface } from '@n8n/decorators';
|
||||||
import { BackendModule } from '@n8n/decorators';
|
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
|
|
||||||
@@ -31,4 +31,11 @@ export class InsightsModule implements ModuleInterface {
|
|||||||
|
|
||||||
return Container.get(InsightsService).settings();
|
return Container.get(InsightsService).settings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnShutdown()
|
||||||
|
async shutdown() {
|
||||||
|
const { InsightsService } = await import('./insights.service');
|
||||||
|
|
||||||
|
await Container.get(InsightsService).shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type InsightsSummary, type InsightsDateRange } from '@n8n/api-types';
|
import { type InsightsSummary, type InsightsDateRange } from '@n8n/api-types';
|
||||||
import { LicenseState, Logger } from '@n8n/backend-common';
|
import { LicenseState, Logger } from '@n8n/backend-common';
|
||||||
import { OnLeaderStepdown, OnLeaderTakeover, OnShutdown } from '@n8n/decorators';
|
import { OnLeaderStepdown, OnLeaderTakeover } from '@n8n/decorators';
|
||||||
import { Service } from '@n8n/di';
|
import { Service } from '@n8n/di';
|
||||||
import { InstanceSettings } from 'n8n-core';
|
import { InstanceSettings } from 'n8n-core';
|
||||||
import { UserError } from 'n8n-workflow';
|
import { UserError } from 'n8n-workflow';
|
||||||
@@ -55,7 +55,6 @@ export class InsightsService {
|
|||||||
this.pruningService.stopPruningTimer();
|
this.pruningService.stopPruningTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnShutdown()
|
|
||||||
async shutdown() {
|
async shutdown() {
|
||||||
await this.collectionService.shutdown();
|
await this.collectionService.shutdown();
|
||||||
this.stopCompactionAndPruningTimers();
|
this.stopCompactionAndPruningTimers();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
import { ShutdownMetadata } from '@n8n/decorators';
|
||||||
import type { ShutdownServiceClass } from '@n8n/decorators';
|
import type { ShutdownServiceClass } from '@n8n/decorators';
|
||||||
import { Container } from '@n8n/di';
|
import { Container } from '@n8n/di';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
@@ -16,11 +16,11 @@ describe('ShutdownService', () => {
|
|||||||
let mockComponent: MockComponent;
|
let mockComponent: MockComponent;
|
||||||
let onShutdownSpy: jest.SpyInstance;
|
let onShutdownSpy: jest.SpyInstance;
|
||||||
const errorReporter = mock<ErrorReporter>();
|
const errorReporter = mock<ErrorReporter>();
|
||||||
const shutdownRegistryMetadata = Container.get(ShutdownRegistryMetadata);
|
const shutdownMetadata = Container.get(ShutdownMetadata);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
shutdownRegistryMetadata.clear();
|
shutdownMetadata.clear();
|
||||||
shutdownService = new ShutdownService(mock(), errorReporter, shutdownRegistryMetadata);
|
shutdownService = new ShutdownService(mock(), errorReporter, shutdownMetadata);
|
||||||
mockComponent = new MockComponent();
|
mockComponent = new MockComponent();
|
||||||
Container.set(MockComponent, mockComponent);
|
Container.set(MockComponent, mockComponent);
|
||||||
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Logger } from '@n8n/backend-common';
|
import { Logger } from '@n8n/backend-common';
|
||||||
import type { ShutdownHandler } from '@n8n/decorators';
|
import type { ShutdownHandler } from '@n8n/decorators';
|
||||||
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
import { ShutdownMetadata } from '@n8n/decorators';
|
||||||
import { Container, Service } from '@n8n/di';
|
import { Container, Service } from '@n8n/di';
|
||||||
import { ErrorReporter } from 'n8n-core';
|
import { ErrorReporter } from 'n8n-core';
|
||||||
import { assert, UnexpectedError, UserError } from 'n8n-workflow';
|
import { assert, UnexpectedError, UserError } from 'n8n-workflow';
|
||||||
@@ -23,17 +23,17 @@ export class ShutdownService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly errorReporter: ErrorReporter,
|
private readonly errorReporter: ErrorReporter,
|
||||||
private readonly shutdownRegistry: ShutdownRegistryMetadata,
|
private readonly shutdownMetadata: ShutdownMetadata,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** Registers given listener to be notified when the application is shutting down */
|
/** Registers given listener to be notified when the application is shutting down */
|
||||||
register(priority: number, handler: ShutdownHandler) {
|
register(priority: number, handler: ShutdownHandler) {
|
||||||
this.shutdownRegistry.register(priority, handler);
|
this.shutdownMetadata.register(priority, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Validates that all the registered shutdown handlers are properly configured */
|
/** Validates that all the registered shutdown handlers are properly configured */
|
||||||
validate() {
|
validate() {
|
||||||
const handlers = this.shutdownRegistry.getHandlersByPriority().flat();
|
const handlers = this.shutdownMetadata.getHandlersByPriority().flat();
|
||||||
|
|
||||||
for (const { serviceClass, methodName } of handlers) {
|
for (const { serviceClass, methodName } of handlers) {
|
||||||
if (!Container.has(serviceClass)) {
|
if (!Container.has(serviceClass)) {
|
||||||
@@ -74,7 +74,7 @@ export class ShutdownService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async startShutdown() {
|
private async startShutdown() {
|
||||||
const handlers = Object.values(this.shutdownRegistry.getHandlersByPriority()).reverse();
|
const handlers = Object.values(this.shutdownMetadata.getHandlersByPriority()).reverse();
|
||||||
|
|
||||||
for (const handlerGroup of handlers) {
|
for (const handlerGroup of handlers) {
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
|
|||||||
Reference in New Issue
Block a user