mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +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) {
|
||||
return this.activeModules.includes(moduleName);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ export class ModuleMetadata {
|
||||
this.modules.set(moduleName, moduleEntry);
|
||||
}
|
||||
|
||||
get(moduleName: string) {
|
||||
return this.modules.get(moduleName);
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
return [...this.modules.entries()];
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type ModuleSettings = Record<string, unknown>;
|
||||
|
||||
export interface ModuleInterface {
|
||||
init?(): Promise<void>;
|
||||
shutdown?(): Promise<void>;
|
||||
entities?(): Promise<EntityClass[]>;
|
||||
settings?(): Promise<ModuleSettings>;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Container, Service } from '@n8n/di';
|
||||
|
||||
import { OnShutdown } from '../on-shutdown';
|
||||
import { ShutdownRegistryMetadata } from '../shutdown-registry-metadata';
|
||||
import { ShutdownMetadata } from '../shutdown-metadata';
|
||||
|
||||
describe('OnShutdown', () => {
|
||||
let shutdownRegistryMetadata: ShutdownRegistryMetadata;
|
||||
let shutdownMetadata: ShutdownMetadata;
|
||||
|
||||
beforeEach(() => {
|
||||
shutdownRegistryMetadata = new ShutdownRegistryMetadata();
|
||||
Container.set(ShutdownRegistryMetadata, shutdownRegistryMetadata);
|
||||
jest.spyOn(shutdownRegistryMetadata, 'register');
|
||||
shutdownMetadata = new ShutdownMetadata();
|
||||
Container.set(ShutdownMetadata, shutdownMetadata);
|
||||
jest.spyOn(shutdownMetadata, 'register');
|
||||
});
|
||||
|
||||
it('should register a methods that is decorated with OnShutdown', () => {
|
||||
@@ -19,8 +19,8 @@ describe('OnShutdown', () => {
|
||||
async onShutdown() {}
|
||||
}
|
||||
|
||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(1);
|
||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
||||
expect(shutdownMetadata.register).toHaveBeenCalledTimes(1);
|
||||
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||
methodName: 'onShutdown',
|
||||
serviceClass: TestClass,
|
||||
});
|
||||
@@ -36,12 +36,12 @@ describe('OnShutdown', () => {
|
||||
async two() {}
|
||||
}
|
||||
|
||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledTimes(2);
|
||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
||||
expect(shutdownMetadata.register).toHaveBeenCalledTimes(2);
|
||||
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||
methodName: 'one',
|
||||
serviceClass: TestClass,
|
||||
});
|
||||
expect(shutdownRegistryMetadata.register).toHaveBeenCalledWith(100, {
|
||||
expect(shutdownMetadata.register).toHaveBeenCalledWith(100, {
|
||||
methodName: 'two',
|
||||
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
|
||||
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', () => {
|
||||
|
||||
@@ -3,6 +3,6 @@ export {
|
||||
DEFAULT_SHUTDOWN_PRIORITY,
|
||||
LOWEST_SHUTDOWN_PRIORITY,
|
||||
} from './constants';
|
||||
export { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
||||
export { ShutdownMetadata } from './shutdown-metadata';
|
||||
export { OnShutdown } from './on-shutdown';
|
||||
export type { ShutdownHandler, ShutdownServiceClass } from './types';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Container } from '@n8n/di';
|
||||
import { UnexpectedError } from 'n8n-workflow';
|
||||
|
||||
import { DEFAULT_SHUTDOWN_PRIORITY } from './constants';
|
||||
import { ShutdownRegistryMetadata } from './shutdown-registry-metadata';
|
||||
import { ShutdownMetadata } from './shutdown-metadata';
|
||||
import type { ShutdownServiceClass } from './types';
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ export const OnShutdown =
|
||||
const methodName = String(propertyKey);
|
||||
// TODO: assert that serviceClass is decorated with @Service
|
||||
if (typeof descriptor?.value === 'function') {
|
||||
Container.get(ShutdownRegistryMetadata).register(priority, { serviceClass, methodName });
|
||||
Container.get(ShutdownMetadata).register(priority, { serviceClass, methodName });
|
||||
} else {
|
||||
const name = `${serviceClass.name}.${methodName}()`;
|
||||
throw new UnexpectedError(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { HIGHEST_SHUTDOWN_PRIORITY, LOWEST_SHUTDOWN_PRIORITY } from './constants
|
||||
import type { ShutdownHandler } from './types';
|
||||
|
||||
@Service()
|
||||
export class ShutdownRegistryMetadata {
|
||||
export class ShutdownMetadata {
|
||||
private handlersByPriority: ShutdownHandler[][] = [];
|
||||
|
||||
register(priority: number, handler: ShutdownHandler) {
|
||||
@@ -1,14 +1,10 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import { SettingsRepository } from '@n8n/db';
|
||||
import { OnPubSubEvent, OnShutdown } from '@n8n/decorators';
|
||||
import { OnPubSubEvent } from '@n8n/decorators';
|
||||
import { Service } from '@n8n/di';
|
||||
import { Cipher, type IExternalSecretsManager } from 'n8n-core';
|
||||
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 {
|
||||
EXTERNAL_SECRETS_DB_KEY,
|
||||
EXTERNAL_SECRETS_INITIAL_BACKOFF,
|
||||
@@ -18,6 +14,10 @@ import { ExternalSecretsProviders } from './external-secrets-providers.ee';
|
||||
import { ExternalSecretsConfig } from './external-secrets.config';
|
||||
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()
|
||||
export class ExternalSecretsManager implements IExternalSecretsManager {
|
||||
private providers: Record<string, SecretsProvider> = {};
|
||||
@@ -65,7 +65,6 @@ export class ExternalSecretsManager implements IExternalSecretsManager {
|
||||
this.logger.debug('External secrets manager initialized');
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
shutdown() {
|
||||
clearInterval(this.updateInterval);
|
||||
Object.values(this.providers).forEach((p) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ModuleInterface } from '@n8n/decorators';
|
||||
import { BackendModule } from '@n8n/decorators';
|
||||
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
@BackendModule({ name: 'external-secrets', licenseFlag: 'feat:externalSecrets' })
|
||||
@@ -16,4 +16,11 @@ export class ExternalSecretsModule implements ModuleInterface {
|
||||
await externalSecretsManager.init();
|
||||
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 { BackendModule } from '@n8n/decorators';
|
||||
import { BackendModule, OnShutdown } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { InstanceSettings } from 'n8n-core';
|
||||
|
||||
@@ -31,4 +31,11 @@ export class InsightsModule implements ModuleInterface {
|
||||
|
||||
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 { 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 { InstanceSettings } from 'n8n-core';
|
||||
import { UserError } from 'n8n-workflow';
|
||||
@@ -55,7 +55,6 @@ export class InsightsService {
|
||||
this.pruningService.stopPruningTimer();
|
||||
}
|
||||
|
||||
@OnShutdown()
|
||||
async shutdown() {
|
||||
await this.collectionService.shutdown();
|
||||
this.stopCompactionAndPruningTimers();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
||||
import { ShutdownMetadata } from '@n8n/decorators';
|
||||
import type { ShutdownServiceClass } from '@n8n/decorators';
|
||||
import { Container } from '@n8n/di';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
@@ -16,11 +16,11 @@ describe('ShutdownService', () => {
|
||||
let mockComponent: MockComponent;
|
||||
let onShutdownSpy: jest.SpyInstance;
|
||||
const errorReporter = mock<ErrorReporter>();
|
||||
const shutdownRegistryMetadata = Container.get(ShutdownRegistryMetadata);
|
||||
const shutdownMetadata = Container.get(ShutdownMetadata);
|
||||
|
||||
beforeEach(() => {
|
||||
shutdownRegistryMetadata.clear();
|
||||
shutdownService = new ShutdownService(mock(), errorReporter, shutdownRegistryMetadata);
|
||||
shutdownMetadata.clear();
|
||||
shutdownService = new ShutdownService(mock(), errorReporter, shutdownMetadata);
|
||||
mockComponent = new MockComponent();
|
||||
Container.set(MockComponent, mockComponent);
|
||||
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Logger } from '@n8n/backend-common';
|
||||
import type { ShutdownHandler } from '@n8n/decorators';
|
||||
import { ShutdownRegistryMetadata } from '@n8n/decorators';
|
||||
import { ShutdownMetadata } from '@n8n/decorators';
|
||||
import { Container, Service } from '@n8n/di';
|
||||
import { ErrorReporter } from 'n8n-core';
|
||||
import { assert, UnexpectedError, UserError } from 'n8n-workflow';
|
||||
@@ -23,17 +23,17 @@ export class ShutdownService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly errorReporter: ErrorReporter,
|
||||
private readonly shutdownRegistry: ShutdownRegistryMetadata,
|
||||
private readonly shutdownMetadata: ShutdownMetadata,
|
||||
) {}
|
||||
|
||||
/** Registers given listener to be notified when the application is shutting down */
|
||||
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 */
|
||||
validate() {
|
||||
const handlers = this.shutdownRegistry.getHandlersByPriority().flat();
|
||||
const handlers = this.shutdownMetadata.getHandlersByPriority().flat();
|
||||
|
||||
for (const { serviceClass, methodName } of handlers) {
|
||||
if (!Container.has(serviceClass)) {
|
||||
@@ -74,7 +74,7 @@ export class ShutdownService {
|
||||
}
|
||||
|
||||
private async startShutdown() {
|
||||
const handlers = Object.values(this.shutdownRegistry.getHandlersByPriority()).reverse();
|
||||
const handlers = Object.values(this.shutdownMetadata.getHandlersByPriority()).reverse();
|
||||
|
||||
for (const handlerGroup of handlers) {
|
||||
await Promise.allSettled(
|
||||
|
||||
Reference in New Issue
Block a user