mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
fix(core): add option for async logger compatibility
This commit is contained in:
@@ -51,4 +51,21 @@ describe('enableShutdownHooks', () => {
|
|||||||
expect(result.stdout.toString().trim()).to.be.eq('');
|
expect(result.stdout.toString().trim()).to.be.eq('');
|
||||||
done();
|
done();
|
||||||
}).timeout(10000);
|
}).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,6 +7,7 @@ import {
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
const SIGNAL = process.argv[2];
|
const SIGNAL = process.argv[2];
|
||||||
const SIGNAL_TO_LISTEN = process.argv[3];
|
const SIGNAL_TO_LISTEN = process.argv[3];
|
||||||
|
const USE_GRACEFUL_EXIT = process.argv[4] === 'graceful';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
class TestInjectable
|
class TestInjectable
|
||||||
@@ -29,10 +30,12 @@ class AppModule {}
|
|||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule, { logger: false });
|
const app = await NestFactory.create(AppModule, { logger: false });
|
||||||
|
|
||||||
|
const shutdownOptions = USE_GRACEFUL_EXIT ? { useProcessExit: true } : {};
|
||||||
|
|
||||||
if (SIGNAL_TO_LISTEN && SIGNAL_TO_LISTEN !== 'NONE') {
|
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') {
|
} else if (SIGNAL_TO_LISTEN !== 'NONE') {
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks([], shutdownOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.listen(1800);
|
await app.listen(1800);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export * from './nest-application-options.interface';
|
|||||||
export * from './nest-application.interface';
|
export * from './nest-application.interface';
|
||||||
export * from './nest-microservice.interface';
|
export * from './nest-microservice.interface';
|
||||||
export * from './scope-options.interface';
|
export * from './scope-options.interface';
|
||||||
|
export * from './shutdown-hooks-options.interface';
|
||||||
export * from './type.interface';
|
export * from './type.interface';
|
||||||
export * from './version-options.interface';
|
export * from './version-options.interface';
|
||||||
export * from './websockets/web-socket-adapter.interface';
|
export * from './websockets/web-socket-adapter.interface';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ShutdownSignal } from '../enums/shutdown-signal.enum';
|
|||||||
import { LoggerService, LogLevel } from '../services/logger.service';
|
import { LoggerService, LogLevel } from '../services/logger.service';
|
||||||
import { DynamicModule } from './modules';
|
import { DynamicModule } from './modules';
|
||||||
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
|
import { NestApplicationContextOptions } from './nest-application-context-options.interface';
|
||||||
|
import { ShutdownHooksOptions } from './shutdown-hooks-options.interface';
|
||||||
import { Type } from './type.interface';
|
import { Type } from './type.interface';
|
||||||
|
|
||||||
export type SelectOptions = Pick<NestApplicationContextOptions, 'abortOnError'>;
|
export type SelectOptions = Pick<NestApplicationContextOptions, 'abortOnError'>;
|
||||||
@@ -143,9 +144,15 @@ export interface INestApplicationContext {
|
|||||||
* `onApplicationShutdown` function of a provider if the
|
* `onApplicationShutdown` function of a provider if the
|
||||||
* process receives a shutdown signal.
|
* 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
|
* @returns {this} The Nest application context instance
|
||||||
*/
|
*/
|
||||||
enableShutdownHooks(signals?: ShutdownSignal[] | string[]): this;
|
enableShutdownHooks(
|
||||||
|
signals?: ShutdownSignal[] | string[],
|
||||||
|
options?: ShutdownHooksOptions,
|
||||||
|
): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the Nest application.
|
* Initializes the Nest application.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
DynamicModule,
|
DynamicModule,
|
||||||
GetOrResolveOptions,
|
GetOrResolveOptions,
|
||||||
SelectOptions,
|
SelectOptions,
|
||||||
|
ShutdownHooksOptions,
|
||||||
Type,
|
Type,
|
||||||
} from '@nestjs/common/interfaces';
|
} from '@nestjs/common/interfaces';
|
||||||
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
import { NestApplicationContextOptions } from '@nestjs/common/interfaces/nest-application-context-options.interface';
|
||||||
@@ -316,10 +317,14 @@ export class NestApplicationContext<
|
|||||||
* process receives a shutdown signal.
|
* process receives a shutdown signal.
|
||||||
*
|
*
|
||||||
* @param {ShutdownSignal[]} [signals=[]] The system signals it should listen to
|
* @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
|
* @returns {this} The Nest application context instance
|
||||||
*/
|
*/
|
||||||
public enableShutdownHooks(signals: (ShutdownSignal | string)[] = []): this {
|
public enableShutdownHooks(
|
||||||
|
signals: (ShutdownSignal | string)[] = [],
|
||||||
|
options: ShutdownHooksOptions = {},
|
||||||
|
): this {
|
||||||
if (isEmpty(signals)) {
|
if (isEmpty(signals)) {
|
||||||
signals = Object.keys(ShutdownSignal).map(
|
signals = Object.keys(ShutdownSignal).map(
|
||||||
(key: string) => ShutdownSignal[key],
|
(key: string) => ShutdownSignal[key],
|
||||||
@@ -336,7 +341,7 @@ export class NestApplicationContext<
|
|||||||
.filter(signal => !this.activeShutdownSignals.includes(signal))
|
.filter(signal => !this.activeShutdownSignals.includes(signal))
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
this.listenToShutdownSignals(signals);
|
this.listenToShutdownSignals(signals, options);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,8 +356,12 @@ export class NestApplicationContext<
|
|||||||
* process events
|
* process events
|
||||||
*
|
*
|
||||||
* @param {string[]} signals The system signals it should listen to
|
* @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;
|
let receivedSignal = false;
|
||||||
const cleanup = async (signal: string) => {
|
const cleanup = async (signal: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -368,7 +377,15 @@ export class NestApplicationContext<
|
|||||||
await this.dispose();
|
await this.dispose();
|
||||||
await this.callShutdownHook(signal);
|
await this.callShutdownHook(signal);
|
||||||
signals.forEach(sig => process.removeListener(sig, cleanup));
|
signals.forEach(sig => process.removeListener(sig, cleanup));
|
||||||
|
|
||||||
|
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);
|
process.kill(process.pid, signal);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Logger.error(
|
Logger.error(
|
||||||
MESSAGES.ERROR_DURING_SHUTDOWN,
|
MESSAGES.ERROR_DURING_SHUTDOWN,
|
||||||
|
|||||||
@@ -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 { expect } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { setTimeout } from 'timers/promises';
|
import { setTimeout } from 'timers/promises';
|
||||||
@@ -154,6 +154,56 @@ describe('NestApplicationContext', () => {
|
|||||||
|
|
||||||
clock.restore();
|
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', () => {
|
describe('get', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user