Seppuku (named after the highly ritual suicide ceremony of Japanese samurai) is a simple module that streamlines the process of gracefully shutting down worker processes in a Node.js cluster that serves web pages through restify or express.
It can be triggered manually, in response to an abnormal condition (e.g.: an unhandled exception), or automatically after a configurable number of requests to keep memory creep at bay.
By default, it stops the server from accepting connections, instructs the parent process to respawn, and gives existing connections a period of time to shut down, after which it automatically terminates the worker process.
To help prevent accidental hosing of your servers, these shutdown periods can be randomized so that two workers started at roughly the same time are less likely to enter seppuku at the same time.
npm install seppuku
var restify = require('restify');
var seppuku = require('seppuku');
var server = restify.createServer();
server.use(seppuku(server, options));
// Go about your business. You're done!
Seppuku's functionality can be altered by passing a number of options, listed below together with their respective defaults:
{
minDeferralTime: 5000, // Minimum time to wait between seppuku and kaishaku
maxDeferralTime: 10000, // Maximum time to wait between seppuku and kaishaku
maxRequests: 0, // Number of requests after which seppuku starts automatically (0 = never)
trapExceptions: true, // Whether exceptions should start seppuku
filter: null, // A filter function that determines the relative weight of a request
kaishakunin: null, // An optional function that replaces the default termination handler
exitCode: 1 // The code to pass to process.exit()
};
When a seppuku operation starts, the default kaishakunin
(“beheader”) method shuts down the server and instructs the worker to disconnect from the parent process (which should then automatically spawn a new worker). Before terminating the worker process, kaishakunin()
waits a variable amount of time to allow existing requests to be terminated gracefully, after which, if any requests are still open, it calls process.exit(1)
.
The termination time is randomized in order to minimize the impact that multiple workers started on or around the same time could have if they entered a seppuku at the same time. You can provide a set of minDeferralTime
and maxDeferralTime
options to determine the randomization boundaries.
Seppuku has the ability to restart automatically after a certain number of requests have been processed by the server. This can be helpful if you have a creeping memory leak that you cannot attend to right away. You can set the maxRequest
option to any arbitrary number, or to zero if you want to turn off this functionality altogether.
By default, seppuku increments the request counter by one every time a request is processed, regardless of its type. If you wish, you can specify a filter
function that receives the current request alongside the current request count and can pass a different weight value depending on the type of request. For example, if you don't want HEAD
and OPTIONS
requests to be counted towards the request count, you could write this filter:
var options = {
maxRequests: 10000,
filter: function(req, requestCount) {
switch(req.method.toLowerCase()) {
case 'head':
case 'options':
return 0;
}
return 1;
}
}
seppuku(server, options);
The default termination handler performs the following:
- Determine and set a timer for the termination time.
- Emit the
seppuku
event on the server instance - Close the server
- Disconnect the worker (this step is safely skipped if the process is not running in a cluster)
- Terminate the process if the timer expires
Note that the timer itself is not retained; if all outstanding events (including existing requests) are resolved before the termination time, the process will terminate before the timer has expired.:
If the default termination handler doesn't do everything you need, you have a couple of options. The first is to trap the seppuku
event on the server object, which is emitted as soon as the seppuku process begins. It receives two parameters, the first the amount of time until the process will be terminated, and the second a cancellation callback (more about this later).
Here's an example:
server.on('seppuku', function(count, cancel, err) {
// Terminate outstanding long-running connections
terminateAllConnections(); // ...
// `err` contains the exception that caused seppuku to start, if any
});
If you desire a completely custom termination process, you can pass your own kaishakunin
method as an option—at which point you're completely on your own. For example:
var options = {
kaishakunin: function(err) {
// Terminate immediately
process.exit(255);
// `err` contains the exception that caused seppuku to start, if any
}
}
Simply call server.seppuku()
, and all will be handled for you.
You can terminate a seppuku operation by trapping the seppuku
event emitted by your server instance and calling the callback that the handler receives as its second parameter. This causes seppuku to be completely reset as if you were restarting the server.
server.on('seppuku', function(count, cancel, err) {
if (someCondition) {
cancel();
}
});
Contributions are welcome, particularly if accompanied by a unit test.
Copyright © 2013 Marco Tabini & Associates, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.