refactor(core): Add shutdown method to modules (#17322)

This commit is contained in:
Iván Ovejero
2025-07-16 12:36:11 +02:00
committed by GitHub
parent a417159602
commit 320a810b56
13 changed files with 68 additions and 35 deletions

View File

@@ -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);
}

View File

@@ -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()];
}

View File

@@ -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>;
}

View File

@@ -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', () => {

View File

@@ -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';

View File

@@ -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(

View File

@@ -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) {