├── .editorconfig ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── circle.yml ├── examples └── hapi.js ├── index.js ├── package.json └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Roy Lines 2 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 3 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hapi-graceful-pm2 2 | [![Circle CI](https://circleci.com/gh/roylines/hapi-graceful-pm2.svg?style=svg)](https://circleci.com/gh/roylines/hapi-graceful-pm2) 3 | [![Coverage Status](https://coveralls.io/repos/roylines/hapi-graceful-pm2/badge.svg?branch=master&service=github)](https://coveralls.io/github/roylines/hapi-graceful-pm2?branch=master) 4 | [![npm version](https://badge.fury.io/js/hapi-graceful-pm2.svg)](https://badge.fury.io/js/hapi-graceful-pm2) 5 | 6 | This is a [hapi plugin](http://hapijs.com/tutorials/plugins) to handle true zero downtime reloads when issuing 7 | a [pm2 reload](http://pm2.keymetrics.io/docs/usage/cluster-mode/#reload-without-downtime) command. 8 | 9 | When using this plugin and calling 'pm2 reload', the 'SIGINT' message will be intercepted and will 10 | wait for hapi to drain all connections before exiting the worker. 11 | This will ensure any in progress requests are completed before exiting. 12 | Whilst waiting, no new requests will be forwarded to the worker. 13 | 14 | Without this plugin the issuing of a reload will terminate any in progress requests without waiting. 15 | You can pass a timeout that configures the maximum time hapi should wait to drain all connections. 16 | Note: the PM2_GRACEFUL_TIMEOUT environment variable should be set to a value higher than the plugin timeout to ensure pm2 doesn't timeout before hapi. 17 | 18 | The pm2 shutdown process is described [here](http://pm2.keymetrics.io/docs/usage/cluster-mode/#reload-without-downtime). 19 | 20 | ## Usage 21 | Register the plugin in the usual way, for instance: 22 | 23 | ``` 24 | server.register({ 25 | plugin: require('hapi-graceful-pm2'), 26 | options: { 27 | timeout: 4000 28 | } 29 | }).then((err) => { 30 | }); 31 | ``` 32 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 8.9.0 4 | 5 | deployment: 6 | production: 7 | branch: master 8 | commands: 9 | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 10 | - npm run coveralls 11 | - npm version "1.0.$CIRCLE_BUILD_NUM" --no-git-tag-version 12 | - npm publish 13 | -------------------------------------------------------------------------------- /examples/hapi.js: -------------------------------------------------------------------------------- 1 | const Hapi = require('hapi'); 2 | 3 | const server = new Hapi.Server({ port: 3000 }); 4 | 5 | const initialize = async (server) => { 6 | await server.register({ 7 | plugin: require('../index.js'), 8 | options: { 9 | timeout: 4000 10 | } 11 | }); 12 | 13 | server.route({ 14 | method: 'GET', 15 | path: '/', 16 | handler(request, h) { 17 | return 'Hello, world!'; 18 | } 19 | }); 20 | 21 | await server.start(); 22 | 23 | return server; 24 | } 25 | 26 | initialize(server).then(server => { 27 | console.log(`Server started at ${server.info.uri}`); 28 | }).catch(err => { 29 | throw err; 30 | }) 31 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const register = (server, options) => { 2 | process.on('SIGINT', async () => { 3 | server.log(['info', 'pm2', 'shutdown'], 'stopping hapi...'); 4 | await server.stop(options); 5 | server.log(['info', 'pm2', 'shutdown'], 'hapi stopped'); 6 | 7 | return process.exit(0); 8 | }); 9 | }; 10 | 11 | exports.default = { 12 | register, 13 | pkg: require('./package.json') 14 | }; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-graceful-pm2", 3 | "version": "1.0.13", 4 | "description": "hapi plugin to handle graceful pm2 reloads", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "lab -c -t 100", 8 | "coveralls": "lab -t 100 -r lcov | coveralls" 9 | }, 10 | "git-pre-hooks": { 11 | "pre-push": "npm test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/roylines/hapi-graceful-pm2.git" 16 | }, 17 | "keywords": [ 18 | "hapi", 19 | "pm2", 20 | "gracefulReload", 21 | "graceful", 22 | "shutdown", 23 | "restart", 24 | "zero downtime" 25 | ], 26 | "author": "Roy Lines (http://roylines.co.uk)", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/roylines/hapi-graceful-pm2/issues" 30 | }, 31 | "homepage": "https://github.com/roylines/hapi-graceful-pm2#readme", 32 | "devDependencies": { 33 | "chai": "*", 34 | "coveralls": "*", 35 | "git-pre-hooks": "*", 36 | "lab": "*", 37 | "sinon": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // jshint esnext:true,node:true 2 | "use strict"; 3 | 4 | const Lab = require("lab"); 5 | const lab = exports.lab = Lab.script(); 6 | const plugin = require('../index.js').default; 7 | const should = require('chai').should(); 8 | const sinon = require('sinon'); 9 | 10 | lab.experiment("hapi-graceful-pm2", () => { 11 | let server = {}; 12 | 13 | let options = {}; 14 | 15 | lab.beforeEach(() => { 16 | sinon.stub(process, 'on').returns(); 17 | sinon.stub(process, 'exit'); 18 | server = { 19 | stop: sinon.stub() 20 | }; 21 | server.log = sinon.stub(); 22 | 23 | return plugin.register(server, options); 24 | }); 25 | 26 | lab.afterEach(() => { 27 | process.on.restore(); 28 | process.exit.restore(); 29 | }); 30 | 31 | lab.test("should have correct attributes", () => { 32 | let pkg = require('../package.json'); 33 | plugin.pkg.should.deep.equal(pkg); 34 | }); 35 | 36 | lab.test("should bind to correct process method", () => { 37 | process.on.args[0][0].should.equal('SIGINT'); 38 | }); 39 | 40 | lab.test("should stop server if shutdown", () => { 41 | server.log.returns(); 42 | server.stop.returns(Promise.resolve()); 43 | process.exit.restore(); 44 | sinon.stub(process, 'exit').callsFake(code => { 45 | code.should.equal(0); 46 | server.stop.calledOnce.should.be.true; 47 | }); 48 | 49 | let method = process.on.args[0][1]; 50 | method('shutdown'); 51 | }); 52 | 53 | lab.test("should not stop server if any other message", () => { 54 | let method = process.on.args[0][1]; 55 | method('any other message'); 56 | }); 57 | 58 | }); 59 | --------------------------------------------------------------------------------