Skip to content

Commit 21ff797

Browse files
core: add retry logic to read streams
1 parent 82bd654 commit 21ff797

6 files changed

Lines changed: 436 additions & 394 deletions

File tree

lib/common/util.js

Lines changed: 100 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ var request = require('request').defaults({
2929
maxSockets: Infinity
3030
}
3131
});
32+
var retryRequest = require('retry-request');
33+
var streamForward = require('stream-forward');
34+
var through = require('through2');
3235
var uuid = require('node-uuid');
3336

3437
/** @const {object} gcloud-node's package.json file. */
@@ -178,34 +181,68 @@ nodeutil.inherits(ApiError, Error);
178181
*/
179182
function handleResp(err, resp, body, callback) {
180183
callback = callback || noop;
184+
181185
if (err) {
182186
callback(err);
183187
return;
184188
}
185-
if (typeof body === 'string') {
186-
try {
187-
body = JSON.parse(body);
188-
} catch(err) {}
189-
}
190-
if (body && body.error) {
191-
// Error from JSON api.
192-
callback(new ApiError(body.error));
189+
190+
var parsedApiResponse = util.parseApiResp(resp, body);
191+
192+
if (parsedApiResponse.err) {
193+
callback(parsedApiResponse.err);
193194
return;
194195
}
195-
if (resp && (resp.statusCode < 200 || resp.statusCode > 299)) {
196+
197+
callback(null, parsedApiResponse.body, parsedApiResponse.resp);
198+
}
199+
200+
util.handleResp = handleResp;
201+
202+
/**
203+
* From an HTTP response, generate an error if one occurred.
204+
*
205+
* @param {*} resp - Response value.
206+
* @param {*=} body - Body value.
207+
* @return {object} parsedResponse - The parsed response.
208+
* @param {?error} parsedResponse.err - An error detected.
209+
* @param {object} parsedResponse.resp - The original response object.
210+
* @param {*} parsedREsponse.body - The original body value provided will try to
211+
* be JSON.parse'd. If it's successful, the parsed value will be returned
212+
* here, otherwise the original value.
213+
*/
214+
function parseApiResp(resp, body) {
215+
var parsedResponse = {
216+
err: null,
217+
resp: resp,
218+
body: body
219+
};
220+
221+
if (resp.statusCode < 200 || resp.statusCode > 299) {
196222
// Unknown error. Format according to ApiError standard.
197-
callback(new ApiError({
223+
parsedResponse.err = new ApiError({
198224
errors: [],
199225
code: resp.statusCode,
200-
message: body || 'Error during request.',
226+
message: 'Error during request.',
201227
response: resp
202-
}));
203-
return;
228+
});
229+
}
230+
231+
if (util.is(body, 'string')) {
232+
try {
233+
parsedResponse.body = JSON.parse(body);
234+
} catch(err) {}
204235
}
205-
callback(null, body, resp);
236+
237+
if (parsedResponse.body && parsedResponse.body.error) {
238+
// Error from JSON API.
239+
parsedResponse.err = new ApiError(parsedResponse.body.error);
240+
}
241+
242+
return parsedResponse;
206243
}
207244

208-
util.handleResp = handleResp;
245+
util.parseApiResp = parseApiResp;
209246

210247
/**
211248
* Get the type of a value.
@@ -418,20 +455,6 @@ function makeWritableStream(dup, options, onComplete) {
418455

419456
util.makeWritableStream = makeWritableStream;
420457

421-
/**
422-
* Returns an exponential distributed time to wait given the number of retries
423-
* that have been previously been attempted on the request.
424-
*
425-
* @param {number} retryNumber - The number of retries previously attempted.
426-
* @return {number} An exponentially distributed time to wait E.g. for use with
427-
* exponential backoff.
428-
*/
429-
function getNextRetryWait(retryNumber) {
430-
return (Math.pow(2, retryNumber) * 1000) + Math.floor(Math.random() * 1000);
431-
}
432-
433-
util.getNextRetryWait = getNextRetryWait;
434-
435458
/**
436459
* Returns true if the API request should be retried, given the error that was
437460
* given the first time the request was attempted. This is used for rate limit
@@ -600,30 +623,47 @@ function makeAuthorizedRequestFactory(config) {
600623
* request options.
601624
*/
602625
function makeAuthorizedRequest(reqOpts, callback) {
603-
if (config.customEndpoint) {
604-
// Using a custom API override. Do not use `google-auth-library` for
605-
// authentication. (ex: connecting to a local Datastore server)
606-
if (callback.onAuthorized) {
607-
callback.onAuthorized(null, reqOpts);
608-
} else {
609-
util.makeRequest(reqOpts, config, callback);
610-
}
626+
var streamMode = !callback;
627+
var stream;
628+
var reqConfig = extend({}, config);
611629

612-
return;
630+
callback = callback || util.noop;
631+
632+
if (streamMode) {
633+
stream = through();
634+
reqConfig.stream = stream;
613635
}
614636

615-
util.authorizeRequest(config, reqOpts, function(err, authorizedReqOpts) {
637+
function onAuthorized(err, authorizedReqOpts) {
616638
if (err) {
617-
(callback.onAuthorized || callback)(err);
639+
if (streamMode) {
640+
stream.emit('error', err);
641+
stream.end();
642+
} else {
643+
(callback.onAuthorized || callback)(err);
644+
}
645+
618646
return;
619647
}
620648

621649
if (callback.onAuthorized) {
622650
callback.onAuthorized(null, authorizedReqOpts);
623651
} else {
624-
util.makeRequest(authorizedReqOpts, config, callback);
652+
util.makeRequest(authorizedReqOpts, reqConfig, callback);
625653
}
626-
});
654+
}
655+
656+
if (reqConfig.customEndpoint) {
657+
// Using a custom API override. Do not use `google-auth-library` for
658+
// authentication. (ex: connecting to a local Datastore server)
659+
onAuthorized(null, reqOpts);
660+
} else {
661+
util.authorizeRequest(reqConfig, reqOpts, onAuthorized);
662+
}
663+
664+
if (streamMode) {
665+
return stream;
666+
}
627667
}
628668

629669
makeAuthorizedRequest.getCredentials = function(callback) {
@@ -653,8 +693,8 @@ function makeAuthorizedRequestFactory(config) {
653693
util.makeAuthorizedRequestFactory = makeAuthorizedRequestFactory;
654694

655695
/**
656-
* Make a request through the `request` module with built-in error handling and
657-
* exponential back off.
696+
* Make a request through the `retryRequest` module with built-in error handling
697+
* and exponential back off.
658698
*
659699
* @param {object} reqOpts - Request options in the format `request` expects.
660700
* @param {object=} config - Configuration object.
@@ -676,30 +716,26 @@ function makeRequest(reqOpts, config, callback) {
676716

677717
reqOpts = util.decorateRequest(reqOpts);
678718

679-
var MAX_RETRIES = config.maxRetries || 3;
680-
var autoRetry = config.autoRetry !== false ? true : false;
681-
var attemptedRetries = 0;
719+
var options = {
720+
request: request,
682721

683-
function shouldRetry(err) {
684-
return autoRetry &&
685-
MAX_RETRIES > attemptedRetries &&
686-
util.shouldRetryRequest(err);
687-
}
722+
retries: config.autoRetry !== false ? config.maxRetries || 3 : 0,
688723

689-
function makeRateLimitedRequest() {
690-
request(reqOpts, function(err, resp, body) {
691-
util.handleResp(err, resp, body, function(err, body, resp) {
692-
if (shouldRetry(err)) {
693-
var delay = util.getNextRetryWait(attemptedRetries++);
694-
setTimeout(makeRateLimitedRequest, delay);
695-
} else {
696-
callback(err || null, body, resp);
697-
}
698-
});
724+
shouldRetryFn: function(resp) {
725+
var err = util.parseApiResp(resp).err;
726+
return err && util.shouldRetryRequest(err);
727+
}
728+
};
729+
730+
if (config.stream) {
731+
// `streamForward` is used to re-emit the events the request stream receives
732+
// on to the stream the user is holding (config.stream).
733+
streamForward(retryRequest(reqOpts, options)).pipe(config.stream);
734+
} else {
735+
retryRequest(reqOpts, options, function(err, response, body) {
736+
util.handleResp(err, response, body, callback);
699737
});
700738
}
701-
702-
makeRateLimitedRequest();
703739
}
704740

705741
util.makeRequest = makeRequest;

0 commit comments

Comments
 (0)