Skip to content

Commit

Permalink
http: overridable keep-alive behavior of Agent
Browse files Browse the repository at this point in the history
Introduce two overridable `Agent` methods:

* `keepSocketAlive(socket)`
* `reuseSocket(socket, req)`

These methods can be overridden by particular `Agent` class child to
make keep-alive behavior customizable.

Motivation: destroy persisted sockets after some configurable timeout.
It is very non-trivial to do it with available primitives. Such program
will most likely need to poke with undocumented events and methods of
`Agent`. With introduced API such behavior is easy to implement.

Backport-PR-URL: #18168
PR-URL: #13005
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Refael Ackermann <[email protected]>
Reviewed-By: Colin Ihrig <[email protected]>
Reviewed-By: Brian White <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
  • Loading branch information
indutny authored and MylesBorins committed Feb 12, 2018
1 parent 7510b97 commit a1171fd
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 6 deletions.
36 changes: 36 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,42 @@ socket/stream from this function, or by passing the socket/stream to `callback`.

`callback` has a signature of `(err, stream)`.

### agent.keepSocketAlive(socket)
<!-- YAML
added: REPLACEME
-->

* `socket` {net.Socket}

Called when `socket` is detached from a request and could be persisted by the
Agent. Default behavior is to:

```js
socket.unref();
socket.setKeepAlive(agent.keepAliveMsecs);
```

This method can be overridden by a particular `Agent` subclass. If this
method returns a falsy value, the socket will be destroyed instead of persisting
it for use with the next request.

### agent.reuseSocket(socket, request)
<!-- YAML
added: REPLACEME
-->

* `socket` {net.Socket}
* `request` {http.ClientRequest}

Called when `socket` is attached to `request` after being persisted because of
the keep-alive options. Default behavior is to:

```js
socket.ref();
```

This method can be overridden by a particular `Agent` subclass.

### agent.destroy()
<!-- YAML
added: v0.11.4
Expand Down
24 changes: 18 additions & 6 deletions lib/_http_agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,15 @@ function Agent(options) {

if (count > self.maxSockets || freeLen >= self.maxFreeSockets) {
socket.destroy();
} else {
} else if (self.keepSocketAlive(socket)) {
freeSockets = freeSockets || [];
self.freeSockets[name] = freeSockets;
socket.setKeepAlive(true, self.keepAliveMsecs);
socket.unref();
socket._httpMessage = null;
self.removeSocket(socket, options);
freeSockets.push(socket);
} else {
// Implementation doesn't want to keep socket alive
socket.destroy();
}
} else {
socket.destroy();
Expand Down Expand Up @@ -142,13 +143,12 @@ Agent.prototype.addRequest = function(req, options) {
if (freeLen) {
// we have a free socket, so use that.
var socket = this.freeSockets[name].shift();
debug('have free socket');

// don't leak
if (!this.freeSockets[name].length)
delete this.freeSockets[name];

socket.ref();
this.reuseSocket(socket, req);
req.onSocket(socket);
this.sockets[name].push(socket);
} else if (sockLen < this.maxSockets) {
Expand Down Expand Up @@ -279,7 +279,19 @@ Agent.prototype.removeSocket = function removeSocket(s, options) {
}
};

Agent.prototype.destroy = function() {
Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) {
socket.setKeepAlive(true, this.keepAliveMsecs);
socket.unref();

return true;
};

Agent.prototype.reuseSocket = function reuseSocket(socket, req) {
debug('have free socket');
socket.ref();
};

Agent.prototype.destroy = function destroy() {
var sets = [this.freeSockets, this.sockets];
for (var s = 0; s < sets.length; s++) {
var set = sets[s];
Expand Down
67 changes: 67 additions & 0 deletions test/parallel/test-http-keepalive-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use strict';
const common = require('../common');
const assert = require('assert');

const http = require('http');

const server = http.createServer((req, res) => {
res.end('ok');
}).listen(0, common.mustCall(() => {
const agent = http.Agent({
keepAlive: true,
maxSockets: 5,
maxFreeSockets: 2
});

const keepSocketAlive = agent.keepSocketAlive;
const reuseSocket = agent.reuseSocket;

let called = 0;
let expectedSocket;
agent.keepSocketAlive = common.mustCall((socket) => {
assert(socket);

called++;
if (called === 1) {
return false;
} else if (called === 2) {
expectedSocket = socket;
return keepSocketAlive.call(agent, socket);
}

assert.strictEqual(socket, expectedSocket);
return false;
}, 3);

agent.reuseSocket = common.mustCall((socket, req) => {
assert.strictEqual(socket, expectedSocket);
assert(req);

return reuseSocket.call(agent, socket, req);
}, 1);

function req(callback) {
http.request({
method: 'GET',
path: '/',
agent,
port: server.address().port
}, common.mustCall((res) => {
res.resume();
res.once('end', common.mustCall(() => {
setImmediate(callback);
}));
})).end();
}

// Should destroy socket instead of keeping it alive
req(common.mustCall(() => {
// Should keep socket alive
req(common.mustCall(() => {
// Should reuse the socket
req(common.mustCall(() => {
server.close();
}));
}));
}));
}));

0 comments on commit a1171fd

Please sign in to comment.