Skip to content

Commit 072602b

Browse files
committed
core: implement retry strategy for grpc requests
1 parent e9e4170 commit 072602b

2 files changed

Lines changed: 94 additions & 12 deletions

File tree

lib/common/grpc-service.js

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ var grpc = require('grpc');
2525
var is = require('is');
2626
var nodeutil = require('util');
2727
var path = require('path');
28+
var retryRequest = require('retry-request');
2829

2930
/**
3031
* @type {module:common/service}
@@ -109,7 +110,8 @@ var HTTP_ERROR_CODE_MAP = {
109110

110111
14: {
111112
code: 503,
112-
message: 'Service Unavailable'
113+
message: 'Service Unavailable',
114+
shouldRetry: true
113115
},
114116

115117
15: {
@@ -123,6 +125,12 @@ var HTTP_ERROR_CODE_MAP = {
123125
}
124126
};
125127

128+
/**
129+
* @const {number} - Allowed number of a request retries
130+
* @private
131+
*/
132+
var MAX_RETRIES = 2;
133+
126134
/**
127135
* Service is a base class, meant to be inherited from by a "service," like
128136
* BigQuery or Storage.
@@ -241,19 +249,29 @@ GrpcService.prototype.request = function(protoOpts, reqOpts, callback) {
241249
grpcOpts.deadline = new Date(Date.now() + protoOpts.timeout);
242250
}
243251

244-
service[protoOpts.method](reqOpts, function(err, resp) {
245-
if (err) {
246-
if (HTTP_ERROR_CODE_MAP[err.code]) {
247-
var httpError = HTTP_ERROR_CODE_MAP[err.code];
248-
err.code = httpError.code;
249-
}
252+
var attempts = 0;
250253

251-
callback(err);
252-
return;
253-
}
254+
(function makeRequest() {
255+
service[protoOpts.method](reqOpts, function(err, resp) {
256+
if (err) {
257+
if (HTTP_ERROR_CODE_MAP[err.code]) {
258+
var httpError = HTTP_ERROR_CODE_MAP[err.code];
259+
260+
if (httpError.shouldRetry && attempts++ < MAX_RETRIES) {
261+
setTimeout(makeRequest, retryRequest.getNextRetryDelay(attempts));
262+
return;
263+
}
264+
265+
err.code = httpError.code;
266+
}
267+
268+
callback(err);
269+
return;
270+
}
254271

255-
callback(null, resp);
256-
}, null, grpcOpts);
272+
callback(null, resp);
273+
}, null, grpcOpts);
274+
}());
257275
};
258276

259277
/**

test/common/grpc-service.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,70 @@ describe('GrpcService', function() {
550550
});
551551
});
552552
});
553+
554+
describe('retrying requests', function() {
555+
var UNAVAILABLE = 14;
556+
var _setTimeout;
557+
558+
before(function() {
559+
_setTimeout = global.setTimeout;
560+
global.setTimeout = function(func) {
561+
func();
562+
};
563+
});
564+
565+
after(function() {
566+
global.setTimeout = _setTimeout;
567+
});
568+
569+
it('should retry if the service is unavailable', function(done) {
570+
var callCount = 0;
571+
572+
grpcService.protos.Service = {
573+
service: function() {
574+
return {
575+
method: function(reqOpts, callback) {
576+
var err = null;
577+
578+
if (++callCount < 2) {
579+
err = { code: UNAVAILABLE };
580+
}
581+
582+
callback(err);
583+
}
584+
};
585+
}
586+
};
587+
588+
grpcService.request(PROTO_OPTS, REQ_OPTS, function(err) {
589+
assert.ifError(err);
590+
assert.strictEqual(callCount, 2);
591+
done();
592+
});
593+
});
594+
595+
it('should retry a maximum of 2 times before failing', function(done) {
596+
var callCount = 0;
597+
598+
grpcService.protos.Service = {
599+
service: function() {
600+
return {
601+
method: function(reqOpts, callback) {
602+
callCount += 1;
603+
callback({ code: UNAVAILABLE });
604+
}
605+
};
606+
}
607+
};
608+
609+
grpcService.request(PROTO_OPTS, REQ_OPTS, function(err) {
610+
assert.strictEqual(err.code, 503);
611+
// 1 for the original request + 2 retries
612+
assert.strictEqual(callCount, 3);
613+
done();
614+
});
615+
});
616+
});
553617
});
554618

555619
describe('convertValue_', function() {

0 commit comments

Comments
 (0)