Skip to content

Commit cfb3f34

Browse files
fix: drop bus subscribers that stopped reading
A `pm2 logs` or `pm2 monit` client that stops reading pub.sock (suspended with Ctrl+Z, dead ssh session) fills the kernel socket buffer, after which PubSocket.send queues every log line of every app in the daemon memory without any limit. The God Daemon then dies with "Reached heap limit" and its fork children are left behind as orphans. PubSocket gets a `max pending bytes` setting: once a peer has more than that waiting in its write queue it is destroyed (it may reconnect) and a `slow subscriber` event is emitted. The daemon enables it with 16 MB by default, configurable with PM2_PUB_MAX_PENDING_BYTES (0 disables), and logs the drop in pm2.log. Refs #5145 #6113 #5802 #5917 #4737 #4647 #3049
1 parent 5377bac commit cfb3f34

4 files changed

Lines changed: 116 additions & 14 deletions

File tree

constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ var csts = {
105105
KILL_SIGNAL : process.env.PM2_KILL_SIGNAL || 'SIGINT',
106106
KILL_USE_MESSAGE : process.env.PM2_KILL_USE_MESSAGE || false,
107107

108+
// Drop a bus (pub.sock) subscriber once this many bytes are waiting for it,
109+
// otherwise a client that stopped reading (`pm2 logs` suspended with Ctrl+Z,
110+
// dead ssh session) makes the daemon buffer every log line until OOM. 0 disables.
111+
PUB_MAX_PENDING_BYTES : isNaN(parseInt(process.env.PM2_PUB_MAX_PENDING_BYTES)) ? 16 * 1024 * 1024 : parseInt(process.env.PM2_PUB_MAX_PENDING_BYTES),
112+
108113
PM2_PROGRAMMATIC : typeof(process.env.pm_id) !== 'undefined' || process.env.PM2_PROGRAMMATIC,
109114
PM2_LOG_DATE_FORMAT : process.env.PM2_LOG_DATE_FORMAT !== undefined ? process.env.PM2_LOG_DATE_FORMAT : 'YYYY-MM-DDTHH:mm:ss'
110115

lib/Daemon.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,13 @@ Daemon.prototype.innerStart = function(cb) {
104104
*/
105105
this.pub = axon.socket('pub-emitter');
106106

107+
// A subscriber that stopped reading (`pm2 logs` suspended with Ctrl+Z, dead
108+
// ssh session) must not make the daemon queue the whole log stream in memory
109+
this.pub.sock.set('max pending bytes', cst.PUB_MAX_PENDING_BYTES);
110+
this.pub.sock.on('slow subscriber', function(sock, pending) {
111+
console.log('Bus subscriber is not reading (%d bytes pending), dropping it', pending);
112+
});
113+
107114
this.pub_socket = this.pub.bind(this.pub_socket_file);
108115

109116
this.pub_socket.once('bind', function() {

modules/pm2-axon/lib/sockets/pub.js

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module.exports = PubSocket;
2020

2121
function PubSocket() {
2222
Socket.call(this);
23+
this.set('max pending bytes', Infinity);
2324
}
2425

2526
/**
@@ -28,6 +29,40 @@ function PubSocket() {
2829

2930
PubSocket.prototype.__proto__ = Socket.prototype;
3031

32+
/**
33+
* Write `buf` to `sock`, unless the peer stopped reading.
34+
*
35+
* A subscriber that does not drain its socket (a `pm2 logs` suspended
36+
* with Ctrl+Z, a dead ssh session) fills the kernel buffer, after which
37+
* every message is queued in this process memory without any limit.
38+
* Once more than `max pending bytes` are waiting for a peer, the peer is
39+
* dropped instead (it may reconnect) and a `slow subscriber` event is
40+
* emitted with the socket and the number of pending bytes.
41+
*
42+
* @param {net.Socket} sock
43+
* @param {Buffer} buf
44+
* @param {Function} [cb]
45+
* @return {Boolean} true if `buf` was handed to `sock`
46+
* @api private
47+
*/
48+
49+
PubSocket.prototype.writeTo = function(sock, buf, cb){
50+
if (!sock.writable) return false;
51+
52+
var max = this.get('max pending bytes');
53+
if (max > 0 && sock.writableLength > max) {
54+
var pending = sock.writableLength;
55+
// destroy first: listeners of the event may publish again (the pm2
56+
// daemon logs through the bus) and must find this peer not writable
57+
sock.destroy();
58+
this.emit('slow subscriber', sock, pending);
59+
return false;
60+
}
61+
62+
sock.write(buf, cb);
63+
return true;
64+
};
65+
3166
/**
3267
* Send `msg` to all established peers.
3368
*
@@ -37,20 +72,19 @@ PubSocket.prototype.__proto__ = Socket.prototype;
3772

3873
PubSocket.prototype.send = function(msg){
3974
var socks = this.socks;
40-
var len = socks.length;
4175
var buf = this.pack(arguments);
4276

4377
for (var sock of socks) {
44-
if (sock.writable) sock.write(buf);
78+
this.writeTo(sock, buf);
4579
}
4680

4781
return this;
4882
};
4983

5084
PubSocket.prototype.sendv2 = function(data, cb){
85+
var self = this;
5186
var socks = this.socks;
5287
var len = socks.length;
53-
var sock;
5488

5589
if (len == 0)
5690
return process.nextTick(cb);
@@ -59,18 +93,15 @@ PubSocket.prototype.sendv2 = function(data, cb){
5993

6094
var i = 0;
6195

96+
function done() {
97+
i++;
98+
if (i == len)
99+
process.nextTick(cb);
100+
}
101+
62102
socks.forEach(function(sock) {
63-
if (sock.writable)
64-
sock.write(buf, function() {
65-
i++;
66-
if (i == len)
67-
process.nextTick(cb);
68-
});
69-
else {
70-
i++;
71-
if (i == len)
72-
process.nextTick(cb);
73-
}
103+
if (!self.writeTo(sock, buf, done))
104+
done();
74105
});
75106

76107
return this;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
var axon = require('..')
3+
, net = require('net')
4+
, should = require('should');
5+
6+
// A subscriber that stops reading (like `pm2 logs` suspended with Ctrl+Z)
7+
// must be dropped once `max pending bytes` is exceeded, while a subscriber
8+
// that keeps reading stays connected.
9+
10+
var LIMIT = 1024 * 1024;
11+
12+
var pub = axon.socket('pub');
13+
pub.set('max pending bytes', LIMIT);
14+
15+
var payload = Buffer.alloc(64 * 1024, 'x');
16+
17+
pub.bind(4200, function(){
18+
var reader = net.connect(4200);
19+
reader.on('data', function(){});
20+
21+
var stuck = net.connect(4200);
22+
stuck.on('connect', function(){
23+
stuck.pause();
24+
25+
var dropped = null;
26+
pub.on('slow subscriber', function(sock, pending){
27+
pending.should.be.above(LIMIT);
28+
sock.remotePort.should.equal(stuck.localPort);
29+
dropped = sock;
30+
});
31+
32+
// peers are registered on the server side asynchronously
33+
setTimeout(function(){
34+
pub.socks.length.should.equal(2);
35+
36+
var sent = 0;
37+
var timer = setInterval(function(){
38+
for (var i = 0; i < 8 && !dropped; i++) {
39+
pub.send(payload);
40+
sent++;
41+
}
42+
43+
if (dropped) {
44+
clearInterval(timer);
45+
// the stuck peer is gone, the reading one is kept
46+
setTimeout(function(){
47+
pub.socks.length.should.equal(1);
48+
pub.socks[0].remotePort.should.equal(reader.localPort);
49+
reader.destroy();
50+
stuck.destroy();
51+
pub.close();
52+
}, 100);
53+
} else if (sent > 20000) {
54+
throw new Error('slow subscriber was not dropped after ' + sent + ' messages');
55+
}
56+
}, 5);
57+
}, 50);
58+
});
59+
});

0 commit comments

Comments
 (0)