From 198080ee1368f7d1e3acdff728e29655e9477ee4 Mon Sep 17 00:00:00 2001 From: Livio Brunner Date: Mon, 27 Nov 2023 13:49:00 +0100 Subject: [PATCH] test: add e2e test for graceful shutdown (#2450) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add e2e test for graceful shutdown Co-authored-by: François <32224751+Lp-Francois@users.noreply.github.com> --------- Co-authored-by: François <32224751+Lp-Francois@users.noreply.github.com> --- e2e/graceful-shutdown.e2e-spec.ts | 66 +++++++++++++++++++ e2e/helper/bootstrap-testing-module.ts | 9 ++- .../graceful-shutdown-timeout.service.ts | 2 +- lib/terminus.module.ts | 36 ++++++---- 4 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 e2e/graceful-shutdown.e2e-spec.ts diff --git a/e2e/graceful-shutdown.e2e-spec.ts b/e2e/graceful-shutdown.e2e-spec.ts new file mode 100644 index 000000000..1ba134702 --- /dev/null +++ b/e2e/graceful-shutdown.e2e-spec.ts @@ -0,0 +1,66 @@ +import { ShutdownSignal } from '@nestjs/common'; +import { type NestApplicationContext } from '@nestjs/core'; +import * as request from 'supertest'; +import { bootstrapTestingModule } from './helper'; +import { sleep } from '../lib/utils'; + +describe('Graceful shutdown', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should gracefully shutdown the application', async () => { + jest.spyOn(global, 'setTimeout'); + const setHealthEndpoint = bootstrapTestingModule({ + gracefulShutdownTimeoutMs: 64, + }).setHealthEndpoint; + + const app = await setHealthEndpoint(({ healthCheck }) => + healthCheck.check([]), + ).start(); + + const { status } = await request(app.getHttpServer()).get('/health'); + + expect(status).toBe(200); + + let isClosed = false; + (app.close as NestApplicationContext['close'])(ShutdownSignal.SIGTERM).then( + () => { + isClosed = true; + }, + ); + + await sleep(16); + // 1. setTimeout is called by the `GracefulShutdownService` + // 2. setTimeout is called above + expect(setTimeout).toHaveBeenCalledTimes(2); + expect(isClosed).toBe(false); + await sleep(16); + expect(isClosed).toBe(false); + await sleep(16); + expect(isClosed).toBe(false); + await sleep(64); + expect(isClosed).toBe(true); + }); + + it('should not delay the shutdown if the application if the timeout is 0', async () => { + jest.spyOn(global, 'setTimeout'); + const setHealthEndpoint = bootstrapTestingModule({ + gracefulShutdownTimeoutMs: 0, + }).setHealthEndpoint; + + const app = await setHealthEndpoint(({ healthCheck }) => + healthCheck.check([]), + ).start(); + + const { status } = await request(app.getHttpServer()).get('/health'); + + expect(status).toBe(200); + + await (app.close as NestApplicationContext['close'])( + ShutdownSignal.SIGTERM, + ); + + expect(setTimeout).not.toHaveBeenCalled(); + }); +}); diff --git a/e2e/helper/bootstrap-testing-module.ts b/e2e/helper/bootstrap-testing-module.ts index 055399b4a..c02c53d74 100644 --- a/e2e/helper/bootstrap-testing-module.ts +++ b/e2e/helper/bootstrap-testing-module.ts @@ -25,6 +25,7 @@ import { SequelizeHealthIndicator, TerminusModule, TypeOrmHealthIndicator, + type TerminusModuleOptions, } from '../../lib'; import { type HealthCheckOptions } from '../../lib/health-check'; import { MikroOrmHealthIndicator } from '../../lib/health-indicator/database/mikro-orm.health'; @@ -92,8 +93,12 @@ export type DynamicHealthEndpointFn = ( ): Promise; }; -export function bootstrapTestingModule() { - const imports: PropType = [TerminusModule]; +export function bootstrapTestingModule( + terminusModuleOptions: TerminusModuleOptions = {}, +) { + const imports: PropType = [ + TerminusModule.forRoot(terminusModuleOptions), + ]; const setHealthEndpoint: DynamicHealthEndpointFn = (func, options = {}) => { const testingModule = Test.createTestingModule({ diff --git a/lib/graceful-shutdown-timeout/graceful-shutdown-timeout.service.ts b/lib/graceful-shutdown-timeout/graceful-shutdown-timeout.service.ts index 3d84f3686..6d6f9bbeb 100644 --- a/lib/graceful-shutdown-timeout/graceful-shutdown-timeout.service.ts +++ b/lib/graceful-shutdown-timeout/graceful-shutdown-timeout.service.ts @@ -29,7 +29,7 @@ export class GracefulShutdownService implements BeforeApplicationShutdown { } async beforeApplicationShutdown(signal: string) { - this.logger.log(`Received termination signal ${signal}`); + this.logger.log(`Received termination signal ${signal || ''}`); if (signal === 'SIGTERM') { this.logger.log( diff --git a/lib/terminus.module.ts b/lib/terminus.module.ts index 591942be2..5cc1ebb66 100644 --- a/lib/terminus.module.ts +++ b/lib/terminus.module.ts @@ -1,5 +1,8 @@ -import { type DynamicModule, Module } from '@nestjs/common'; -import { TERMINUS_GRACEFUL_SHUTDOWN_TIMEOUT } from './graceful-shutdown-timeout/graceful-shutdown-timeout.service'; +import { type DynamicModule, Module, type Provider } from '@nestjs/common'; +import { + GracefulShutdownService, + TERMINUS_GRACEFUL_SHUTDOWN_TIMEOUT, +} from './graceful-shutdown-timeout/graceful-shutdown-timeout.service'; import { HealthCheckService } from './health-check'; import { getErrorLoggerProvider } from './health-check/error-logger/error-logger.provider'; import { ERROR_LOGGERS } from './health-check/error-logger/error-loggers.provider'; @@ -9,7 +12,7 @@ import { DiskUsageLibProvider } from './health-indicator/disk/disk-usage-lib.pro import { HEALTH_INDICATORS } from './health-indicator/health-indicators.provider'; import { type TerminusModuleOptions } from './terminus-options.interface'; -const providers = [ +const baseProviders: Provider[] = [ ...ERROR_LOGGERS, DiskUsageLibProvider, HealthCheckExecutor, @@ -26,7 +29,7 @@ const exports_ = [HealthCheckService, ...HEALTH_INDICATORS]; * @publicApi */ @Module({ - providers: [...providers, getErrorLoggerProvider(), getLoggerProvider()], + providers: [...baseProviders, getErrorLoggerProvider(), getLoggerProvider()], exports: exports_, }) export class TerminusModule { @@ -37,17 +40,24 @@ export class TerminusModule { gracefulShutdownTimeoutMs = 0, } = options; + const providers: Provider[] = [ + ...baseProviders, + getErrorLoggerProvider(errorLogStyle), + getLoggerProvider(logger), + ]; + + if (gracefulShutdownTimeoutMs > 0) { + providers.push({ + provide: TERMINUS_GRACEFUL_SHUTDOWN_TIMEOUT, + useValue: gracefulShutdownTimeoutMs, + }); + + providers.push(GracefulShutdownService); + } + return { module: TerminusModule, - providers: [ - ...providers, - getErrorLoggerProvider(errorLogStyle), - getLoggerProvider(logger), - { - provide: TERMINUS_GRACEFUL_SHUTDOWN_TIMEOUT, - useValue: gracefulShutdownTimeoutMs, - }, - ], + providers, exports: exports_, }; }