Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions integration/hooks/e2e/enable-shutdown-hook.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,21 @@ describe('enableShutdownHooks', () => {
expect(result.stdout.toString().trim()).to.be.eq('');
done();
}).timeout(10000);

it('should call the correct hooks with useProcessExit option', done => {
const result = spawnSync('ts-node', [
join(__dirname, '../src/enable-shutdown-hooks-main.ts'),
'SIGHUP',
'SIGHUP',
'graceful',
]);
const calls = result.stdout
.toString()
.split('\n')
.map((call: string) => call.trim());
expect(calls[0]).to.equal('beforeApplicationShutdown SIGHUP');
expect(calls[1]).to.equal('onApplicationShutdown SIGHUP');
expect(result.status).to.equal(0);
done();
}).timeout(10000);
});
7 changes: 5 additions & 2 deletions integration/hooks/src/enable-shutdown-hooks-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { NestFactory } from '@nestjs/core';
const SIGNAL = process.argv[2];
const SIGNAL_TO_LISTEN = process.argv[3];
const USE_GRACEFUL_EXIT = process.argv[4] === 'graceful';

@Injectable()
class TestInjectable
Expand All @@ -29,10 +30,12 @@ class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule, { logger: false });

const shutdownOptions = USE_GRACEFUL_EXIT ? { useProcessExit: true } : {};

if (SIGNAL_TO_LISTEN && SIGNAL_TO_LISTEN !== 'NONE') {
app.enableShutdownHooks([SIGNAL_TO_LISTEN]);
app.enableShutdownHooks([SIGNAL_TO_LISTEN], shutdownOptions);
} else if (SIGNAL_TO_LISTEN !== 'NONE') {
app.enableShutdownHooks();
app.enableShutdownHooks([], shutdownOptions);
}

await app.listen(1800);
Expand Down
1 change: 1 addition & 0 deletions packages/common/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './nest-application-options.interface';
export * from './nest-application.interface';
export * from './nest-microservice.interface';
export * from './scope-options.interface';
export * from './shutdown-hooks-options.interface';
export * from './type.interface';
export * from './version-options.interface';
export * from './websockets/web-socket-adapter.interface';
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ShutdownSignal } from '../enums/shutdown-signal.enum';
import { LoggerService, LogLevel } from '../services/logger.service';
import { DynamicModule } from './modules';
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
import { ShutdownHooksOptions } from './shutdown-hooks-options.interface';
import { Type } from './type.interface';

export type SelectOptions = Pick<NestApplicationContextOptions, 'abortOnError'>;
Expand Down Expand Up @@ -143,9 +144,15 @@ export interface INestApplicationContext {
* `onApplicationShutdown` function of a provider if the
* process receives a shutdown signal.
*
* @param {ShutdownSignal[] | string[]} [signals] The system signals to listen to
* @param {ShutdownHooksOptions} [options] Options for configuring shutdown hooks behavior
*
* @returns {this} The Nest application context instance
*/
enableShutdownHooks(signals?: ShutdownSignal[] | string[]): this;
enableShutdownHooks(
signals?: ShutdownSignal[] | string[],
options?: ShutdownHooksOptions,
): this;

/**
* Initializes the Nest application.
Expand Down
21 changes: 21 additions & 0 deletions packages/common/interfaces/shutdown-hooks-options.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Options for configuring shutdown hooks behavior.
*
* @publicApi
*/
export interface ShutdownHooksOptions {
/**
* If true, uses `process.exit()` instead of `process.kill(process.pid, signal)`
* after shutdown hooks complete. This ensures the 'exit' event is properly
* triggered, which is required for async loggers (like Pino with transports)
* to flush their buffers before the process terminates.
*
* Note: Using `process.exit()` will:
* - Change the exit code (e.g., SIGTERM: 143 → 0)
* - May not trigger other signal handlers from third-party libraries
* - May affect orchestrator (Kubernetes, Docker) behavior
*
* @default false
*/
useProcessExit?: boolean;
}
25 changes: 21 additions & 4 deletions packages/core/nest-application-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
DynamicModule,
GetOrResolveOptions,
SelectOptions,
ShutdownHooksOptions,
Type,
} from '@nestjs/common/interfaces';
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
Expand Down Expand Up @@ -316,10 +317,14 @@ export class NestApplicationContext<
* process receives a shutdown signal.
*
* @param {ShutdownSignal[]} [signals=[]] The system signals it should listen to
* @param {ShutdownHooksOptions} [options={}] Options for configuring shutdown hooks behavior
*
* @returns {this} The Nest application context instance
*/
public enableShutdownHooks(signals: (ShutdownSignal | string)[] = []): this {
public enableShutdownHooks(
signals: (ShutdownSignal | string)[] = [],
options: ShutdownHooksOptions = {},
): this {
if (isEmpty(signals)) {
signals = Object.keys(ShutdownSignal).map(
(key: string) => ShutdownSignal[key],
Expand All @@ -336,7 +341,7 @@ export class NestApplicationContext<
.filter(signal => !this.activeShutdownSignals.includes(signal))
.toArray();

this.listenToShutdownSignals(signals);
this.listenToShutdownSignals(signals, options);
return this;
}

Expand All @@ -351,8 +356,12 @@ export class NestApplicationContext<
* process events
*
* @param {string[]} signals The system signals it should listen to
* @param {ShutdownHooksOptions} options Options for configuring shutdown hooks behavior
*/
protected listenToShutdownSignals(signals: string[]) {
protected listenToShutdownSignals(
signals: string[],
options: ShutdownHooksOptions = {},
) {
let receivedSignal = false;
const cleanup = async (signal: string) => {
try {
Expand All @@ -368,7 +377,15 @@ export class NestApplicationContext<
await this.dispose();
await this.callShutdownHook(signal);
signals.forEach(sig => process.removeListener(sig, cleanup));
process.kill(process.pid, signal);

if (options.useProcessExit) {
// Use process.exit() to ensure the 'exit' event is properly triggered.
// This is required for async loggers (like Pino with transports)
// to flush their buffers before the process terminates.
process.exit(0);
} else {
process.kill(process.pid, signal);
}
} catch (err) {
Logger.error(
MESSAGES.ERROR_DURING_SHUTDOWN,
Expand Down
52 changes: 51 additions & 1 deletion packages/core/test/nest-application-context.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InjectionToken, Provider, Scope, Injectable } from '@nestjs/common';
import { Injectable, InjectionToken, Provider, Scope } from '@nestjs/common';
import { expect } from 'chai';
import * as sinon from 'sinon';
import { setTimeout } from 'timers/promises';
Expand Down Expand Up @@ -154,6 +154,56 @@ describe('NestApplicationContext', () => {

clock.restore();
});

it('should use process.exit when useProcessExit option is enabled', async () => {
const signal = 'SIGTERM';
const applicationContext = await testHelper(A, Scope.DEFAULT);

const processExitStub = sinon.stub(process, 'exit');
const processKillStub = sinon.stub(process, 'kill');

applicationContext.enableShutdownHooks([signal], {
useProcessExit: true,
});

const hookStub = sinon
.stub(applicationContext as any, 'callShutdownHook')
.callsFake(async () => undefined);

const shutdownCleanupRef = applicationContext['shutdownCleanupRef']!;
await shutdownCleanupRef(signal);

hookStub.restore();
processExitStub.restore();
processKillStub.restore();

expect(processExitStub.calledOnceWith(0)).to.be.true;
expect(processKillStub.called).to.be.false;
});

it('should use process.kill when useProcessExit option is not enabled', async () => {
const signal = 'SIGTERM';
const applicationContext = await testHelper(A, Scope.DEFAULT);

const processExitStub = sinon.stub(process, 'exit');
const processKillStub = sinon.stub(process, 'kill');

applicationContext.enableShutdownHooks([signal]);

const hookStub = sinon
.stub(applicationContext as any, 'callShutdownHook')
.callsFake(async () => undefined);

const shutdownCleanupRef = applicationContext['shutdownCleanupRef']!;
await shutdownCleanupRef(signal);

hookStub.restore();
processExitStub.restore();
processKillStub.restore();

expect(processKillStub.calledOnceWith(process.pid, signal)).to.be.true;
expect(processExitStub.called).to.be.false;
});
});

describe('get', () => {
Expand Down
Loading