-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Disgraceful shutdown, request before signal interrupted with ECONNREFUSED #11416
Comments
So, I could be wrong, but what I have found is that the |
Hello, what's the update on this issue?? |
I think that the problem lies on the express adapter itself, on the close method. public close() {
this.closeOpenConnections();
if (!this.httpServer) {
return undefined;
}
return new Promise(resolve => this.httpServer.close(resolve));
} As you can see, this As you can see in the express documentation (https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html), the |
cc @tolgap 👀 @coderhammer this method call won't have any impact if |
Honestly I'm not sure, I just tried on a new project and it closes open connections with the default. Here is the current implementation of the private closeOpenConnections() {
for (const socket of this.openConnections) {
socket.destroy();
this.openConnections.delete(socket);
}
} A quick fix for me for now is to implement my own import { Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
import { HttpAdapterHost } from '@nestjs/core';
@Injectable()
export class AppService implements OnModuleDestroy {
constructor(private readonly httpAdapter: HttpAdapterHost) {}
async onModuleDestroy() {
Logger.debug('Module destroyed called!');
const httpServer = this.httpAdapter.httpAdapter.getHttpServer();
await new Promise((resolve) => httpServer.close(resolve));
Logger.debug('Waited for all connections to be closed!');
}
} EDIT: The behaviour is happening when activating |
|
I'm so sorry, it works exactly as expected when trying again on a fresh project. I was trying something with a special configuration. Have a nice day! This is not an issue for me anymore. |
@coderhammer what did you change? Is there a workaround? |
Honestly, everything works fine on my side when testing manually. I tried your reproduction repo but I think that it's more a test configuration that is failing :/ |
@coderhammer If you think the issue is with test configuration, where do you think the issue is? I thought it could be that I don't think it's feasible to reproduce this manually (without an automated test), because the timeframe in which this bug occurs is very small, but is enough to be experienced by users in a production system under load. |
Also I don't think that |
After investigating a little bit on your test repo, it seems to me that the problem comes from supertest itself. I'll try to make a reproduction later this week |
@coderhammer Looks like Supertest may be related, but is not the only issue. The same test case (shutting down while a request is in progress) fails on However so does making a request after the server shut down (about which I don't care if it's 503 or |
@soryy708 could you spin up a pure express application (non-NestJS-driven) just to see what's the original behavior for your test? |
With the simplest possible Express application (without even Terminus) it acts the same as with Nest not using Supertest. const express = require('express');
function createServer() {
const app = express();
app.get('/health', async (_req, res) => {
res.status(200).send();
})
return new Promise((resolve, reject) => {
const server = app.listen(0, error => {
if (error) {
reject(error);
return;
}
resolve([server.address().port, () => server.close()]);
});
})
}
describe("API main (no Supertest)", () => {
describe("Lifecycle", () => {
let port;
let close;
beforeEach(async () => {
[port, close] = await createServer();
});
afterEach(async () => {
await close();
});
const probeReadiness = async () => {
const request = new Request(`http://localhost:${port}/health`, { method: 'GET' });
const response = await fetch(request);
return { statusCode: response.status };
}
describe("When bootstrapped", () => {
describe("When running", () => {
it("Should respond successfully", async () => {
const response = await probeReadiness();
expect(response.statusCode).toBe(200);
});
});
describe("When shutting down", () => {
describe("When a request is already in progress", () => {
it("Should respond successfully", async () => {
const responsePromise = probeReadiness();
const closePromise = close();
const [response] = await Promise.all([
responsePromise,
closePromise,
]);
expect(response.statusCode).toBe(200);
});
});
describe("When a new request is made", () => {
it("Should respond with 503 Service Unavailable", async () => {
await close();
const response = await probeReadiness();
expect(response.statusCode).toBe(503);
});
});
});
});
});
}); Result:
Both on I expect that if I use Terminus, it will postpone Express'es shutdown long enough to flush the in-progress requests. |
If this behavior can be reproduced with the pure express application, then I don't think there's anything we can/should do given we don't really want to alter the default driver's behavior. Perhaps reporting this issue in the terminus package would make more sense then? |
Are we sure that it's an issue with the Terminus package? I'm opening an issue in https://github.com/nestjs/terminus. |
Given that this is the default behavior of HTTP drivers we rely on (Nest is built upon), I wouldn't even call this an "issue" but rather a "feature request". Whether it should be implemented & exposed from the terminus package, or an entirely new package that extends the default driver and adds some extra functionality on top, is a different question. |
Whatever you want to call it. It's the default behavior of Express without anything, because Express is not a framework like Nest is. Express docs tell you that if you want graceful shutdown, you need to add another thing to Express (e.g. https://expressjs.com/en/advanced/healthcheck-graceful-shutdown.html The idea is to observe for |
This doesn't imply we should be changing the driver's default behavior, especially by default.
So I'd say exposing this feature from the |
@kamilmysliwiec I have done some investigation on this. It seems Fastify has an "option" for handling things like this. They introduce an option called Then before Fastify performs the close logic, they return a 503 on any open request in their route handler. I can see how this issue pops up for regular health checks. But this can also be useful for SSE! I think we should consider keeping this logic in |
Either way works for me. Since it can be useful for SSE too (and we expose abstractions for SSE from the core) then having it in the platform-specific packages would make sense too! |
Hey @kamilmysliwiec |
Have you tried using this package https://www.npmjs.com/package/nestjs-graceful-shutdown? |
Is there an existing issue for this?
Current behavior
When the application is shutdown (e.g. with SIGTERM shutdown hook), requests that started before shutdown fail with ECONNREFUSED.
Minimum reproduction code
https://github.com/soryy708/nestjs-disgraceful-shutdown-mcve
Steps to reproduce
yarn install
yarn test
Expected behavior
Requests that started before shutdown should succeed, only new requests after shutdown should fail.
Package
@nestjs/common
@nestjs/core
@nestjs/microservices
@nestjs/platform-express
@nestjs/platform-fastify
@nestjs/platform-socket.io
@nestjs/platform-ws
@nestjs/testing
@nestjs/websockets
Other package
No response
NestJS version
9.3.12
Packages versions
Node.js version
16.19.0
In which operating systems have you tested?
Other
No response
The text was updated successfully, but these errors were encountered: