Skip to content

Commit 5e841b8

Browse files
author
Mark Cavage
committedJan 9, 2013
move to node-backoff in HttpClient
1 parent 6c22e94 commit 5e841b8

File tree

6 files changed

+116
-147
lines changed

6 files changed

+116
-147
lines changed
 

‎CHANGES.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## 2.0.5 (not yet released)
44

5-
- Support keep-alive by default in client
5+
- GH-291 fix overwriting `options.type` in createJSONClient (Trent Mick)
6+
- Move to `node-backoff` and rework retry logic in HttpClient
7+
- Support keep-alive by default in HttpClient
68

79
## 2.0.4
810

‎bin/report-latency

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
var fs = require('fs');
55
var path = require('path');
66
var spawn = require('child_process').spawn;
7+
var readline = require('readline');
78
var sprintf = require('util').format;
89

9-
var byline = require('byline');
1010
var nopt = require('nopt');
1111

1212

@@ -145,15 +145,17 @@ var buckets = {};
145145

146146
var done = 0;
147147
parsed.argv.remain.forEach(function (f) {
148-
var stream = byline(fs.createReadStream(f));
149-
stream.on('data', function (l) {
148+
var stream = readline.createInterface({
149+
input: fs.createReadStream(f),
150+
output: null
151+
})
152+
stream.on('line', function (l) {
150153
var record;
151154
var t = -1;
152155

153156
try {
154157
record = JSON.parse(l);
155158
} catch (e) {}
156-
// console.error('%s: line was: "%s"', e, l);
157159

158160
if (!record)
159161
return;

‎lib/clients/http_client.js

+99-126
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ var util = require('util');
1212
var zlib = require('zlib');
1313

1414
var assert = require('assert-plus');
15+
var backoff = require('backoff');
1516
var KeepAliveAgent = require('keep-alive-agent');
1617
var mime = require('mime');
17-
var retry = require('retry');
18+
var once = require('once');
1819
var semver = require('semver');
1920
var uuid = require('node-uuid');
2021

@@ -42,38 +43,84 @@ function defaultUserAgent() {
4243
}
4344

4445

45-
function newRetryOperation(options) {
46-
var operation;
47-
if (options.retry !== false) {
48-
operation = retry.operation(options.retry);
49-
} else {
50-
// Stub out node-retry so the code isn't fugly in the
51-
// mainline client
52-
operation = {
53-
attempt: function (callback) {
54-
return (callback(1));
55-
},
56-
retry: function (err) {
57-
operation._err = err;
58-
return (false);
59-
},
60-
mainError: function () {
61-
return (operation._err);
46+
function ConnectTimeoutError(ms) {
47+
if (Error.captureStackTrace)
48+
Error.captureStackTrace(this, ConnectTimeoutError);
49+
50+
this.message = 'connect timeout after ' + ms + 'ms';
51+
}
52+
util.inherits(ConnectTimeoutError, Error);
53+
54+
55+
function rawRequest(opts, cb) {
56+
assert.object(opts, 'options');
57+
assert.object(opts.log, 'options.log');
58+
assert.func(cb, 'callback');
59+
60+
cb = once(cb);
61+
62+
var log = opts.log;
63+
var proto = opts.protocol === 'https:' ? https : http;
64+
var timer;
65+
66+
if (opts.cert && opts.key)
67+
opts.agent = false;
68+
69+
if (opts.connectTimeout) {
70+
timer = setTimeout(function connectTimeout() {
71+
timer = null;
72+
if (req) {
73+
req.abort();
6274
}
63-
};
75+
76+
cb(new ConnectTimeoutError(opts.connectTimeout), req);
77+
}, opts.connectTimeout);
6478
}
6579

66-
return (operation);
67-
}
80+
var req = proto.request(opts, function onResponse(res) {
81+
clearTimeout(timer);
82+
log.trace({client_res: res}, 'Response received');
6883

84+
res.log = log;
6985

70-
function ConnectTimeoutError() {
71-
if (Error.captureStackTrace)
72-
Error.captureStackTrace(this, ConnectTimeoutError);
86+
var err;
87+
if (res.statusCode >= 400)
88+
err = errors.codeToHttpError(res.statusCode);
7389

74-
this.message = util.format.apply(util, arguments);
75-
}
76-
util.inherits(ConnectTimeoutError, Error);
90+
req.removeAllListeners('error');
91+
req.removeAllListeners('socket');
92+
req.emit('result', (err || null), res);
93+
});
94+
req.log = log;
95+
96+
req.on('error', function onError(err) {
97+
log.trace({err: err}, 'Request failed');
98+
clearTimeout(timer);
99+
100+
cb(err, req);
101+
if (req) {
102+
process.nextTick(function () {
103+
req.emit('result', err, null);
104+
});
105+
}
106+
});
107+
108+
req.once('socket', function onSocket(socket) {
109+
if (socket.writable && !socket._connecting) {
110+
clearTimeout(timer);
111+
cb(null, req);
112+
return;
113+
}
114+
115+
socket.once('connect', function onConnect() {
116+
clearTimeout(timer);
117+
cb(null, req);
118+
});
119+
});
120+
121+
if (opts.signRequest)
122+
opts.signRequest(req);
123+
} // end `rawRequest`
77124

78125

79126

@@ -234,115 +281,39 @@ HttpClient.prototype.basicAuth = function basicAuth(username, password) {
234281
};
235282

236283

237-
HttpClient.prototype.request = function request(options, callback) {
238-
var done = false;
239-
240-
function _callback(err, req) {
241-
if (done)
242-
return (false);
243-
244-
done = true;
245-
return (callback(err, req));
246-
}
284+
HttpClient.prototype.request = function request(opts, cb) {
285+
assert.object(opts, 'options');
286+
assert.func(cb, 'callback');
247287

248-
// Don't rely on x-request-id, as the server may not accept it
249-
var log = this.log;
250-
var operation = newRetryOperation(options);
251-
var proto = options.protocol === 'https:' ? https : http;
252-
var signRequest = this.signRequest;
253-
254-
operation.attempt(function retryCallback(currentAttempt) {
255-
var timer = false;
256-
function clearTimer() {
257-
if (timer)
258-
clearTimeout(timer);
288+
var self = this;
259289

260-
timer = false;
290+
// Make one request, then kick off the next call; note that
291+
// node-backoff always imposes an initial time delay before
292+
// starting, which we don't want.
293+
rawRequest(opts, function _callback(err, req) {
294+
if (!err || opts.retry === false) {
295+
cb(err, req);
296+
return;
261297
}
262298

263-
log.trace({
264-
client_req: options,
265-
attempt: currentAttempt
266-
}, 'sending request');
299+
var retry = backoff.call(rawRequest, opts, cb);
300+
retry.setStrategy(new backoff.ExponentialStrategy({
301+
initialDelay: self.retry.minTimeout || 1000,
302+
maxDelay: self.retry.maxTimeout || Infinity
303+
}));
267304

268-
if (options.cert && options.key)
269-
options.agent = new proto.Agent(options);
270-
271-
var req = proto.request(options, function requestCallback(res) {
272-
clearTimer();
273-
log.trace({client_res: res}, 'Response received');
274-
275-
res.log = log;
276-
277-
var err = null;
278-
if (res.statusCode >= 400)
279-
err = errors.codeToHttpError(res.statusCode);
280-
281-
req.emit('result', err, res);
282-
});
283-
284-
req.log = log;
285-
if (signRequest)
286-
signRequest(req);
287-
288-
req.on('error', function onError(err) {
289-
log.trace({err: err}, 'Request failed');
290-
clearTimer();
291-
if (!done) {
292-
if (!operation.retry(err)) {
293-
err = operation.mainError() || err;
294-
return (_callback(err));
295-
}
296-
} else {
297-
// Since we're changing subtly the way the
298-
// client interacts with callbacks to handle
299-
// socket connections, we have to handle the
300-
// case where the caller was told we're
301-
// "good to go", and instead mimic the behavior
302-
// of there being a bad http code. I.e., a
303-
// server may just TCP RST us instead of a 400
304-
req.emit('result', err, null);
305-
}
306-
307-
return (false);
308-
});
309-
310-
if (options.connectTimeout) {
311-
timer = setTimeout(function connectTimeout() {
312-
clearTimer();
313-
req.abort();
314-
315-
var to = options.connectTimeout;
316-
var err = new ConnectTimeoutError('timeout ' +
317-
'after ' +
318-
to +
319-
'ms');
320-
321-
if (!operation.retry(err)) {
322-
err = operation.mainError() || err;
323-
return (_callback(err));
324-
}
325-
326-
return (false);
327-
}, options.connectTimeout);
305+
if (typeof (opts.retry.retries) === 'number') {
306+
if (opts.retry.retries !== Infinity)
307+
retry.failAfter(opts.retry.retries);
308+
} else {
309+
retry.failAfter(4);
328310
}
329311

330-
req.once('socket', function onSocket(socket) {
331-
if (socket.writable && !socket._connecting) {
332-
clearTimer();
333-
return (_callback(null, req));
334-
}
335-
336-
return socket.once('connect', function onConnect() {
337-
clearTimer();
338-
return (_callback(null, req));
339-
});
340-
});
312+
retry.on('backoff', self.emit.bind(self, 'attempt'));
341313
});
342314
};
343315

344316

345-
346317
HttpClient.prototype._options = function (method, options) {
347318
if (typeof (options) !== 'object')
348319
options = { path: options };
@@ -354,9 +325,11 @@ HttpClient.prototype._options = function (method, options) {
354325
connectTimeout: options.connectTimeout || self.connectTimeout,
355326
headers: options.headers || {},
356327
key: options.key || self.key,
328+
log: options.log || self.log,
357329
method: method,
358330
path: options.path || self.path,
359-
retry: options.retry || self.retry
331+
retry: options.retry || self.retry,
332+
signRequest: options.signRequest || self.signRequest
360333
};
361334

362335
// Backwards compatibility with restify < 1.0

‎lib/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ function createServer(options) {
8989
return (false);
9090
}
9191

92-
res.send(new InternalError(e.message || 'unexpected error'));
92+
res.send(new InternalError(e, e.message || 'unexpected error'));
9393
return (true);
9494
});
9595

‎package.json

+6-7
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,19 @@
3737
},
3838
"dependencies": {
3939
"assert-plus": "0.1.2",
40-
"bunyan": "0.16.8",
41-
"clone": "git://github.com/pvorb/node-clone.git#341dd3f",
42-
"dtrace-provider": "0.2.6",
43-
"byline": "2.0.3",
40+
"backoff": "1.2.0",
41+
"bunyan": "0.17.0",
42+
"clone": "0.1.5",
43+
"dtrace-provider": "0.2.7",
4444
"formidable": "1.0.11",
4545
"http-signature": "0.9.9",
4646
"keep-alive-agent": "0.0.1",
4747
"lru-cache": "2.2.1",
4848
"mime": "1.2.7",
4949
"node-uuid": "1.4.0",
5050
"once": "1.1.1",
51-
"qs": "0.5.2",
52-
"retry": "0.6.0",
53-
"semver": "1.1.1",
51+
"qs": "0.5.3",
52+
"semver": "1.1.2",
5453
"spdy": "1.3.9",
5554
"verror": "1.3.5"
5655
},

‎test/client.test.js

+1-8
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,6 @@ before(function (callback) {
7676
log: helper.getLog('server')
7777
});
7878

79-
SERVER.on('clientError', function (err) {
80-
console.error('CLIENT ERROR');
81-
console.error(err.stack);
82-
// process.exit(1);
83-
});
84-
8579
SERVER.use(restify.acceptParser(['json', 'text/plain']));
8680
SERVER.use(restify.dateParser());
8781
SERVER.use(restify.authorizationParser());
@@ -159,7 +153,7 @@ test('GET json', function (t) {
159153
test('GH-115 GET path with spaces', function (t) {
160154
JSON_CLIENT.get('/json/foo bar', function (err, req, res, obj) {
161155
t.ok(err);
162-
console.log(err);
156+
t.equal(err.code, 'ECONNRESET');
163157
t.end();
164158
});
165159
});
@@ -362,7 +356,6 @@ test('GH-20 connectTimeout', function (t) {
362356

363357
client.get('/foo', function (err, req) {
364358
t.ok(err);
365-
t.notOk(req);
366359
t.end();
367360
});
368361
});

0 commit comments

Comments
 (0)
Please sign in to comment.