222
schangxiang@126.com
2025-06-13 6a8393408d8cefcea02b7a598967de8dc1e565c2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
'use strict';
 
var http = require('http');
var cluster = require('cluster');
var ms = require('humanize-ms');
var pstree = require('ps-tree');
 
/**
 * graceful, please use with `cluster` in production env.
 *
 * @param {Object} options
 *  - {Array<HttpServer>} servers, we need to close it and stop taking new requests.
 *  - {Function(err, throwErrorCount)} [error], when uncaughtException emit, error(err, count).
 *    You can log error here.
 *  - {Number} [killTimeout], worker suicide timeout, default is 30 seconds.
 *  - {Object} [worker], worker contains `disconnect()`.
 */
module.exports = function graceful(options) {
  options = options || {};
  var killTimeout = ms(options.killTimeout || '30s');
  var onError = options.error || function () {};
  var servers = options.servers || options.server || [];
  if (!Array.isArray(servers)) {
    servers = [servers];
  }
  if (servers.length === 0) {
    throw new TypeError('options.servers required!');
  }
 
  var throwErrorCount = 0;
  process.on('uncaughtException', function (err) {
    throwErrorCount += 1;
    onError(err, throwErrorCount);
    console.error('[%s] [graceful:worker:%s:uncaughtException] throw error %d times',
      Date(), process.pid, throwErrorCount);
    console.error(err);
    console.error(err.stack);
    if (throwErrorCount > 1) {
      return;
    }
 
    servers.forEach(function (server) {
      if (server instanceof http.Server) {
        server.on('request', function (req, res) {
          // Let http server set `Connection: close` header, and close the current request socket.
          req.shouldKeepAlive = false;
          res.shouldKeepAlive = false;
          if (!res._header) {
            res.setHeader('Connection', 'close');
          }
        });
      }
    });
 
    // make sure we close down within `killTimeout` seconds
    var killtimer = setTimeout(function () {
      console.error('[%s] [graceful:worker:%s] kill timeout, exit now.', Date(), process.pid);
      if (process.env.NODE_ENV !== 'test') {
        // kill children by SIGKILL before exit
        killChildren(function() {
          process.exit(1);
        });
      }
    }, killTimeout);
    console.error('[%s] [graceful:worker:%s] will exit after %dms', Date(), process.pid, killTimeout);
 
    // But don't keep the process open just for that!
    // If there is no more io waitting, just let process exit normally.
    if (typeof killtimer.unref === 'function') {
      // only worked on node 0.10+
      killtimer.unref();
    }
 
    var worker = options.worker || cluster.worker;
 
    // cluster mode
    if (worker) {
      try {
        // stop taking new requests.
        // because server could already closed, need try catch the error: `Error: Not running`
        for (var i = 0; i < servers.length; i++) {
          var server = servers[i];
          server.close();
          console.error('[%s] [graceful:worker:%s] close server#%s, _connections: %s',
            Date(), process.pid, i, server._connections);
        }
        console.error('[%s] [graceful:worker:%s] close %d servers!',
          Date(), process.pid, servers.length);
      } catch (er1) {
        // Usually, this error throw cause by the active connections after the first domain error,
        // oh well, not much we can do at this point.
        console.error('[%s] [graceful:worker:%s] Error on server close!\n%s',
          Date(), process.pid, er1.stack);
      }
 
      try {
        // Let the master know we're dead.  This will trigger a
        // 'disconnect' in the cluster master, and then it will fork
        // a new worker.
        worker.send('graceful:disconnect');
        worker.disconnect();
        console.error('[%s] [graceful:worker:%s] worker disconnect!',
          Date(), process.pid);
      } catch (er2) {
        // Usually, this error throw cause by the active connections after the first domain error,
        // oh well, not much we can do at this point.
        console.error('[%s] [graceful:worker:%s] Error on worker disconnect!\n%s',
          Date(), process.pid, er2.stack);
      }
    }
  });
};
 
function killChildren(callback) {
  pstree(process.pid, function(err, children) {
    if (err) {
      // if get children error, just ignore it
      console.error('[%s] [graceful:worker:%s] pstree find children error: %s', Date(), process.pid, err);
      callback();
      return;
    }
    children.forEach(function(child) {
      kill(parseInt(child.PID));
    });
    callback();
  });
}
 
function kill(pid) {
  try {
    process.kill(pid, 'SIGKILL');
  } catch (_) {
    // ignore
  }
}