Graceful shutdown
Let’s imagine you have an HTTP server with NodeJS which connected to a database, and every time server gets called then it sends a request to the database to get/set a data which also will send to the client by the response.
Imagine you need to shut down the server, the easiest way to do that is <Ctrl>+C and the server will be killed, but wait, what if your server did not finish all the requests, what if some client connections are closed because the server is killed and can not handle the requests.
-That gives you a point to think, right?
As you might guess you need to handle all requests and close all resources which are processing on data(e.e. database connections) and not take any other requests and after that, you can shut down your server with a quiet conscience.
Graceful shutdown means when all your requests to the server is respond and not any data processing work left.
It’s very important to create a graceful shutdown and shut down your server correctly because you can not know what can happen to the requests that have been made to the server if you shut down the server immediately, you can make mistake and kill the other process (byPID) which provided to the NodeJS server or some other things might happen, which could be bad for your application.
Here are the four steps of how you can do a graceful shutdown in an easy way.
First of all we need to handle in the server that someone wants to shut down, after that we need to complete all the requests and stop taking new requests from users to the server, so we can be sure that there are no any pending requests from users to the server before shutting down the server, after that we need to close all the data processing (i.e. databases, some file system workers, etc..), it depends on what you are doing in the server, and finally we can exit from the process.
Let’s create a simple NodeJS server and do all the steps that I have mentioned above which will do a graceful shutdown when we want to close the server and understand how it works.
Here is a simple NodeJS Server example using ExpressJS
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect('mongodb://localhost/test', (err) => {
if (err) throw err;
console.log('Mongoose connected!');
});
const User = mongoose.model('User', { name: String });
app.post('/user', async (req, res) => {
try {
const user = new User({ name: req.body.username });
await user.save();
res.send('Success!').status(201);
} catch (err) {
res.send(err.message).status(500);
}
});
app.listen(3000, () => console.log('Example app listening on port 3000!'));
Here we have a simple server which has a route that creates a user in MongoDB.
We can test the server and create a user in the database using this command
curl -d ‘{ “username”: “Narek” }’ -H “Content-Type: application/json” -X POST http://localhost:3000/user
If you got
Success!
message then you can look the JSON data in MongoDB.Now let’s go through the four steps and write appropriate code for that.
1. Handle process kill signal
First, let’s understand what is a process signal.
A signal is an asynchronous notification sent to a process or to a specific thread to notify an event that occurred.
Signal events will be emitted when the NodeJS process receives a signal.
Each signal have a name(i.e. 'SIGINT', 'SIGTERM', etc.), more about that in NodeJS here.
'SIGINT' generated with <Ctrl>+C in the terminal.The 'SIGTERM' signal is a generic signal used to cause program termination. Unlike 'SIGKILL' this signal can be blocked, handled, and ignored. It is the normal way to politely ask a program to terminate.The shell command kill generates 'SIGTERM' by default.
You can read more about Termination Signals here.
As you guess we need to add handler which will receive 'SIGTERM' signal.
Here is the next example built on the previous example which handles the signal.
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect('mongodb://localhost/test', (err) => {
if (err) throw err;
console.log('Mongoose connected!');
});
const User = mongoose.model('User', { name: String });
app.post('/user', async (req, res) => {
try {
const user = new User({ name: req.body.username });
await user.save();
res.send('Success!').status(201);
} catch (err) {
res.send(err.message).status(500);
}
});
app.listen(3000, () => console.log('Example app listening on port 3000!'));
process.on('SIGTERM', () => {
console.info('SIGTERM signal received.');
});
Now let’s try and test it.
Run the server, after that you need to get PID number, you get that using ps command, so now you have the number and you can try to kill the server using this command kill [PID_number] or just killall node which will send a signal to all node servers, after running this command you will get this log from node outputs
SIGTERM signal recived.
If you will try again you will get the same log
SIGTERM signal recived.
-Hmmm, why the process has not been killed?
Because you handled the signal and ignored it.
So, the first step is done, let’s move to the next step.
Now we need to stop the http server and stop accepting new requests.
It can be done using server.close function to get more information about that you can have a look also in NodeJS doc.
So, the code will look like this
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect('mongodb://localhost/test', (err) => {
if (err) throw err;
console.log('Mongoose connected!');
});
const User = mongoose.model('User', { name: String });
app.post('/user', async (req, res) => {
try {
const user = new User({ name: req.body.username });
await user.save();
res.send('Success!').status(201);
} catch (err) {
res.send(err.message).status(500);
}
});
const server = app.listen(3000, () => console.log('Example app listening on port 3000!'));
process.on('SIGTERM', () => {
console.info('SIGTERM signal received.');
console.log('Closing http server.');
server.close(() => {
console.log('Http server closed.');
});
});
It will stop accepting new connections to the server and if you will try to call the server your request will fail.
In this example, the point is to close the MongoDB connection because not any requests left to the database.
So it can be done with this code
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect('mongodb://localhost/test', (err) => {
if (err) throw err;
console.log('Mongoose connected!');
});
const User = mongoose.model('User', { name: String });
app.post('/user', async (req, res) => {
try {
const user = new User({ name: req.body.username });
await user.save();
res.send('Success!').status(201);
} catch (err) {
res.send(err.message).status(500);
}
});
const server = app.listen(3000, () => console.log('Example app listening on port 3000!'));
process.on('SIGTERM', () => {
console.info('SIGTERM signal received.');
console.log('Closing http server.');
server.close(() => {
console.log('Http server closed.');
// boolean means [force], see in mongoose doc
mongoose.connection.close(false, () => {
console.log('MongoDb connection closed.');
});
});
});
-Hmmmmmmm, why node server exits after DB connection close.
It’s a very interesting question, you can try to understand that point by yourself, but if you can’t or don’t want I will describe it in the next chapter.
As you have seen our application exits close database connection.
What is the cause? -EventLoop
As we know NodeJS will exit when EventLoop queue is empty and nothing left to do.
But sometimes your application can have more functions and will not exit automatically, in this point comes our last work to do.
We need to exit from process using process.exit function.
And final graceful shutdown server example will look like this
const express = require('express');
const mongoose = require('mongoose');
const app = express();
app.use(express.urlencoded({extended: true}));
app.use(express.json());
mongoose.connect('mongodb://localhost/test', (err) => {
if (err) throw err;
console.log('Mongoose connected!');
});
const User = mongoose.model('User', { name: String });
app.post('/user', async (req, res) => {
try {
const user = new User({ name: req.body.username });
await user.save();
res.send('Success!').status(201);
} catch (err) {
res.send(err.message).status(500);
}
});
const server = app.listen(3000, () => console.log('Example app listening on port 3000!'));
process.on('SIGTERM', () => {
console.info('SIGTERM signal received.');
console.log('Closing http server.');
server.close(() => {
console.log('Http server closed.');
// boolean means [force], see in mongoose doc
mongoose.connection.close(false, () => {
console.log('MongoDb connection closed.');
process.exit(0);
});
});
});
Argument 0 means exit with a “success” code.
To exit with a “failure” code use 1.
To get this exit code after shutdown, run this command in the terminal where you run your node server.
echo $?
By default NodeJS exits with process code 0 if EventLoop is empty.
That’s it!
It’s not the only way to shutdown server correctly, this will work fine on small projects and can be written easily, but I am not saying in big projects, it’s not needed. In big projects more likely using server balancing (i.e. Nginx), you can balance the load and not send any request to that server and shutdown it.
Thank you for reading this article, feel free to ask any questions or tweet me @nairhar.
My article about “NodeJS Health Checks and Overload Protection”
https://medium.com/@nairihar/nodejs-health-checks-and-overload-protection-368a132a725e