feat(core): Unify application components shutdown (#8097)
## Summary Add `ShutdownService` and `OnShutdown` decorator for more unified way to shutdown different components. Use this new way in the following components: - HTTP(S) server - Pruning service - Push connection - License --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
127
packages/cli/test/unit/shutdown/Shutdown.service.test.ts
Normal file
127
packages/cli/test/unit/shutdown/Shutdown.service.test.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { ApplicationError, ErrorReporterProxy } from 'n8n-workflow';
|
||||
import { mock } from 'jest-mock-extended';
|
||||
import type { ServiceClass } from '@/shutdown/Shutdown.service';
|
||||
import { ShutdownService } from '@/shutdown/Shutdown.service';
|
||||
import Container from 'typedi';
|
||||
|
||||
class MockComponent {
|
||||
onShutdown() {}
|
||||
}
|
||||
|
||||
describe('ShutdownService', () => {
|
||||
let shutdownService: ShutdownService;
|
||||
let mockComponent: MockComponent;
|
||||
let onShutdownSpy: jest.SpyInstance;
|
||||
let mockErrorReporterProxy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
shutdownService = new ShutdownService(mock());
|
||||
mockComponent = new MockComponent();
|
||||
Container.set(MockComponent, mockComponent);
|
||||
onShutdownSpy = jest.spyOn(mockComponent, 'onShutdown');
|
||||
mockErrorReporterProxy = jest.spyOn(ErrorReporterProxy, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
describe('shutdown', () => {
|
||||
it('should signal shutdown', () => {
|
||||
shutdownService.register(10, {
|
||||
serviceClass: MockComponent as unknown as ServiceClass,
|
||||
methodName: 'onShutdown',
|
||||
});
|
||||
shutdownService.shutdown();
|
||||
expect(onShutdownSpy).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should signal shutdown in the priority order', async () => {
|
||||
class MockService {
|
||||
onShutdownHighPrio() {}
|
||||
|
||||
onShutdownLowPrio() {}
|
||||
}
|
||||
|
||||
const order: string[] = [];
|
||||
const mockService = new MockService();
|
||||
Container.set(MockService, mockService);
|
||||
|
||||
jest.spyOn(mockService, 'onShutdownHighPrio').mockImplementation(() => order.push('high'));
|
||||
jest.spyOn(mockService, 'onShutdownLowPrio').mockImplementation(() => order.push('low'));
|
||||
|
||||
shutdownService.register(100, {
|
||||
serviceClass: MockService as unknown as ServiceClass,
|
||||
methodName: 'onShutdownHighPrio',
|
||||
});
|
||||
|
||||
shutdownService.register(10, {
|
||||
serviceClass: MockService as unknown as ServiceClass,
|
||||
methodName: 'onShutdownLowPrio',
|
||||
});
|
||||
|
||||
shutdownService.shutdown();
|
||||
await shutdownService.waitForShutdown();
|
||||
expect(order).toEqual(['high', 'low']);
|
||||
});
|
||||
|
||||
it('should throw error if shutdown is already in progress', () => {
|
||||
shutdownService.register(10, {
|
||||
methodName: 'onShutdown',
|
||||
serviceClass: MockComponent as unknown as ServiceClass,
|
||||
});
|
||||
shutdownService.shutdown();
|
||||
expect(() => shutdownService.shutdown()).toThrow('App is already shutting down');
|
||||
});
|
||||
|
||||
it('should report error if component shutdown fails', async () => {
|
||||
const componentError = new Error('Something went wrong');
|
||||
onShutdownSpy.mockImplementation(() => {
|
||||
throw componentError;
|
||||
});
|
||||
shutdownService.register(10, {
|
||||
serviceClass: MockComponent as unknown as ServiceClass,
|
||||
methodName: 'onShutdown',
|
||||
});
|
||||
shutdownService.shutdown();
|
||||
await shutdownService.waitForShutdown();
|
||||
|
||||
expect(mockErrorReporterProxy).toHaveBeenCalledTimes(1);
|
||||
const error = mockErrorReporterProxy.mock.calls[0][0];
|
||||
expect(error).toBeInstanceOf(ApplicationError);
|
||||
expect(error.message).toBe('Failed to shutdown gracefully');
|
||||
expect(error.extra).toEqual({
|
||||
component: 'MockComponent.onShutdown()',
|
||||
});
|
||||
expect(error.cause).toBe(componentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('waitForShutdown', () => {
|
||||
it('should wait for shutdown', async () => {
|
||||
shutdownService.register(10, {
|
||||
serviceClass: MockComponent as unknown as ServiceClass,
|
||||
methodName: 'onShutdown',
|
||||
});
|
||||
shutdownService.shutdown();
|
||||
await expect(shutdownService.waitForShutdown()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw error if app is not shutting down', async () => {
|
||||
await expect(async () => shutdownService.waitForShutdown()).rejects.toThrow(
|
||||
'App is not shutting down',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isShuttingDown', () => {
|
||||
it('should return true if app is shutting down', () => {
|
||||
shutdownService.register(10, {
|
||||
serviceClass: MockComponent as unknown as ServiceClass,
|
||||
methodName: 'onShutdown',
|
||||
});
|
||||
shutdownService.shutdown();
|
||||
expect(shutdownService.isShuttingDown()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if app is not shutting down', () => {
|
||||
expect(shutdownService.isShuttingDown()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user