mirror of
https://github.com/nestjs/nest.git
synced 2026-02-21 23:11:44 +00:00
97
integration/graceful-shutdown/e2e/express.spec.ts
Normal file
97
integration/graceful-shutdown/e2e/express.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ExpressAdapter } from '@nestjs/platform-express';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { expect } from 'chai';
|
||||
import * as http from 'http';
|
||||
import { AppModule } from '../src/app.module';
|
||||
|
||||
describe('Graceful Shutdown (Express)', () => {
|
||||
let app: INestApplication;
|
||||
|
||||
afterEach(async () => {
|
||||
if (app) {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow in-flight requests to complete when gracefulShutdown is enabled', async () => {
|
||||
app = await NestFactory.create(
|
||||
AppModule,
|
||||
new ExpressAdapter() as any,
|
||||
{
|
||||
gracefulShutdown: true,
|
||||
logger: false,
|
||||
} as any,
|
||||
);
|
||||
|
||||
await app.listen(0);
|
||||
const port = app.getHttpServer().address().port;
|
||||
|
||||
const requestPromise = new Promise<string>((resolve, reject) => {
|
||||
http
|
||||
.get(`http://localhost:${port}/slow`, res => {
|
||||
let data = '';
|
||||
res.on('data', c => (data += c));
|
||||
res.on('end', () => resolve(data));
|
||||
})
|
||||
.on('error', reject);
|
||||
});
|
||||
|
||||
// Wait to ensure request is processing
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
const closePromise = app.close();
|
||||
|
||||
// The in-flight request should finish successfully
|
||||
const response = await requestPromise;
|
||||
expect(response).to.equal('ok');
|
||||
|
||||
await closePromise;
|
||||
});
|
||||
|
||||
it('should return 503 for NEW queued requests on existing connections during shutdown', async () => {
|
||||
app = await NestFactory.create(
|
||||
AppModule,
|
||||
new ExpressAdapter() as any,
|
||||
{
|
||||
gracefulShutdown: true,
|
||||
logger: false,
|
||||
} as any,
|
||||
);
|
||||
|
||||
await app.listen(0);
|
||||
const port = app.getHttpServer().address().port;
|
||||
|
||||
// Force 1 socket to ensure queuing/reuse
|
||||
const agent = new http.Agent({ keepAlive: true, maxSockets: 1 });
|
||||
|
||||
// 1. Send Request A (slow) - occupies the socket
|
||||
const req1 = http.get(`http://localhost:${port}/slow`, { agent });
|
||||
|
||||
// 2. Wait so Request A is definitely "in flight"
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
// 3. Trigger Shutdown (don't await yet)
|
||||
const closePromise = app.close();
|
||||
|
||||
// Give NestJS a moment to set the isShuttingDown flag
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
// 4. Send Request B immediately using the same agent.
|
||||
const statusPromise = new Promise<number>((resolve, reject) => {
|
||||
const req = http.get(`http://localhost:${port}/slow`, { agent }, res => {
|
||||
resolve(res.statusCode || 0);
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
|
||||
// 5. Cleanup Request A
|
||||
req1.on('error', () => {});
|
||||
|
||||
const status = await statusPromise;
|
||||
expect(status).to.equal(503);
|
||||
|
||||
await closePromise;
|
||||
agent.destroy();
|
||||
});
|
||||
});
|
||||
11
integration/graceful-shutdown/src/app.controller.ts
Normal file
11
integration/graceful-shutdown/src/app.controller.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@Get('slow')
|
||||
async slow() {
|
||||
// Simulate work
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
7
integration/graceful-shutdown/src/app.module.ts
Normal file
7
integration/graceful-shutdown/src/app.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [AppController],
|
||||
})
|
||||
export class AppModule {}
|
||||
14
integration/graceful-shutdown/tsconfig.json
Normal file
14
integration/graceful-shutdown/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@nestjs/common": ["../../packages/common/index.ts"],
|
||||
"@nestjs/core": ["../../packages/core/index.ts"],
|
||||
"@nestjs/platform-express": ["../../packages/platform-express/index.ts"],
|
||||
"@nestjs/testing": ["../../packages/testing/index.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["e2e/**/*.ts", "src/**/*.ts"]
|
||||
}
|
||||
@@ -30,4 +30,11 @@ export interface NestApplicationOptions extends NestApplicationContextOptions {
|
||||
* keep-alive connections in the HTTP adapter.
|
||||
*/
|
||||
forceCloseConnections?: boolean;
|
||||
/**
|
||||
* Whether to enable graceful shutdown behavior.
|
||||
* When enabled, the server will return 503 Service Unavailable for new requests
|
||||
* during the shutdown process, but allow existing in-flight requests to complete.
|
||||
* @default false
|
||||
*/
|
||||
gracefulShutdown?: boolean;
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ export class ExpressAdapter extends AbstractHttpAdapter<
|
||||
private readonly logger = new Logger(ExpressAdapter.name);
|
||||
private readonly openConnections = new Set<Duplex>();
|
||||
private readonly registeredPrefixes = new Set<string>();
|
||||
private isShuttingDown = false;
|
||||
private onRequestHook?: (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
@@ -243,6 +244,7 @@ export class ExpressAdapter extends AbstractHttpAdapter<
|
||||
}
|
||||
|
||||
public close() {
|
||||
this.isShuttingDown = true;
|
||||
this.closeOpenConnections();
|
||||
|
||||
if (!this.httpServer) {
|
||||
@@ -317,6 +319,7 @@ export class ExpressAdapter extends AbstractHttpAdapter<
|
||||
}
|
||||
|
||||
public initHttpServer(options: NestApplicationOptions) {
|
||||
this.logger.log('[DEBUG] initHttpServer called with:', options);
|
||||
const isHttpsEnabled = options && options.httpsOptions;
|
||||
if (isHttpsEnabled) {
|
||||
this.httpServer = https.createServer(
|
||||
@@ -327,6 +330,22 @@ export class ExpressAdapter extends AbstractHttpAdapter<
|
||||
this.httpServer = http.createServer(this.getInstance());
|
||||
}
|
||||
|
||||
if (options?.gracefulShutdown) {
|
||||
this.logger.log('[DEBUG] Registering graceful shutdown middleware');
|
||||
this.instance.use((req: any, res: any, next: any) => {
|
||||
this.logger.log(
|
||||
`[DEBUG] Middleware hit. isShuttingDown: ${this.isShuttingDown}`,
|
||||
);
|
||||
if (this.isShuttingDown) {
|
||||
this.logger.log('🛑 Middleware Intercepted Request! Sending 503...');
|
||||
res.set('Connection', 'close');
|
||||
res.status(503).send('Service Unavailable');
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options?.forceCloseConnections) {
|
||||
this.trackOpenConnections();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user