Skip to content

Commit

Permalink
add ability to set the max number of retries and the retry interval
Browse files Browse the repository at this point in the history
  • Loading branch information
mattbjordan committed Aug 5, 2022
1 parent 50f645f commit 655e985
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 11 deletions.
6 changes: 4 additions & 2 deletions i18next.js
Expand Up @@ -2002,6 +2002,8 @@
_this.waitingReads = [];
_this.maxParallelReads = options.maxParallelReads || 10;
_this.readingCalls = 0;
_this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5;
_this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350;
_this.state = {};
_this.queue = [];

Expand Down Expand Up @@ -2108,7 +2110,7 @@
var _this3 = this;

var tried = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var wait = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 350;
var wait = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : this.retryTimeout;
var callback = arguments.length > 5 ? arguments[5] : undefined;
if (!lng.length) return callback(null, {});

Expand All @@ -2134,7 +2136,7 @@
_this3.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback);
}

if (err && data && tried < 5) {
if (err && data && tried < _this3.maxRetries) {
setTimeout(function () {
_this3.read.call(_this3, lng, ns, fcName, tried + 1, wait * 2, callback);
}, wait);
Expand Down
2 changes: 1 addition & 1 deletion i18next.min.js

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions index.d.ts
Expand Up @@ -623,6 +623,23 @@ export interface InitOptions extends MergeBy<DefaultPluginOptions, PluginOptions
* @default 10
*/
maxParallelReads?: number;

/**
* The maximum number of retries to perform.
* Note that retries are only performed when a request has no response
* and throws an error.
* The default value is used if value is set below 0.
* @default 5
*/
maxRetries?: number;

/**
* Set how long to wait, in milliseconds, betweeen retries of failed requests.
* This number is compounded by a factor of 2 for subsequent retry.
* The default value is used if value is set below 1ms.
* @default 350
*/
retryTimeout?: number;
}

export interface TOptionsBase {
Expand Down
7 changes: 5 additions & 2 deletions src/BackendConnector.js
Expand Up @@ -27,6 +27,9 @@ class Connector extends EventEmitter {
this.maxParallelReads = options.maxParallelReads || 10;
this.readingCalls = 0;

this.maxRetries = options.maxRetries >= 0 ? options.maxRetries : 5;
this.retryTimeout = options.retryTimeout >= 1 ? options.retryTimeout : 350;

this.state = {};
this.queue = [];

Expand Down Expand Up @@ -139,7 +142,7 @@ class Connector extends EventEmitter {
this.queue = this.queue.filter((q) => !q.done);
}

read(lng, ns, fcName, tried = 0, wait = 350, callback) {
read(lng, ns, fcName, tried = 0, wait = this.retryTimeout, callback) {
if (!lng.length) return callback(null, {}); // noting to load

// Limit parallelism of calls to backend
Expand All @@ -158,7 +161,7 @@ class Connector extends EventEmitter {
const next = this.waitingReads.shift();
this.read(next.lng, next.ns, next.fcName, next.tried, next.wait, next.callback);
}
if (err && data /* = retryFlag */ && tried < 5) {
if (err && data /* = retryFlag */ && tried < this.maxRetries) {
setTimeout(() => {
this.read.call(this, lng, ns, fcName, tried + 1, wait * 2, callback);
}, wait);
Expand Down
198 changes: 194 additions & 4 deletions test/backend/backendConnector.load.retry.spec.js
Expand Up @@ -21,9 +21,9 @@ describe('BackendConnector load retry', () => {

describe('#load', () => {
it('should load data', (done) => {
connector.load(['en'], ['retry'], function (err) {
connector.load(['en'], ['retry2'], function (err) {
expect(err).to.be.not.ok;
expect(connector.store.getResourceBundle('en', 'retry')).to.eql({
expect(connector.store.getResourceBundle('en', 'retry2')).to.eql({
status: 'nok',
retries: 2,
});
Expand Down Expand Up @@ -152,15 +152,15 @@ describe('BackendConnector load only one succeeds with retries', () => {

describe('#load', () => {
it('should call callback', (done) => {
connector.load(['en'], ['fail', 'fail2', 'concurrently', 'retry'], function (err) {
connector.load(['en'], ['fail', 'fail2', 'concurrently', 'retry2'], function (err) {
expect(err).to.eql(['failed loading', 'failed loading']);
expect(connector.store.getResourceBundle('en', 'fail')).to.eql({});
expect(connector.store.getResourceBundle('en', 'fail2')).to.eql({});
expect(connector.store.getResourceBundle('en', 'concurrently')).to.eql({
status: 'ok',
namespace: 'concurrently',
});
expect(connector.store.getResourceBundle('en', 'retry')).to.eql({
expect(connector.store.getResourceBundle('en', 'retry2')).to.eql({
status: 'nok',
retries: 2,
});
Expand Down Expand Up @@ -196,3 +196,193 @@ describe('BackendConnector reload retry', () => {
});
});
});

describe('BackendConnector retry with default maxRetries=5', () => {
let connector;

before(() => {
connector = new BackendConnector(
new BackendMock(),
new ResourceStore(),
{
interpolator: new Interpolator(),
},
{
// retryTimeout: 350, // This is the default value
// maxRetries: 5, // This is the default value
backend: { loadPath: 'http://localhost:9876/locales/{{lng}}/{{ns}}.json' },
},
);
});

describe('#load', () => {
it('retry 1 time', (done) => {
connector.load(['en'], ['retry1'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry1')).to.eql({
status: 'nok',
retries: 1,
});
done();
});
}).timeout(10850);

it('retry 5 times', (done) => {
connector.load(['en'], ['retry5'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry5')).to.eql({
status: 'nok',
retries: 5,
});
done();
});
}).timeout(10850 + 250); // ((2^5) - 1) * 350 = 10850

it('fail after retrying 5 times', (done) => {
connector.load(['en'], ['retry6'], function (err) {
expect(err).to.eql(['failed loading']);
expect(connector.store.getResourceBundle('en', 'retry6')).to.eql({});
done();
});
}).timeout(10850 + 250); // ((2^5) - 1) * 350 = 10850
});
});

describe('BackendConnector retry with maxRetries=6', () => {
let connector;

before(() => {
connector = new BackendConnector(
new BackendMock(),
new ResourceStore(),
{
interpolator: new Interpolator(),
},
{
// retryTimeout: 350, // This is the default value
maxRetries: 6,
backend: { loadPath: 'http://localhost:9876/locales/{{lng}}/{{ns}}.json' },
},
);
});

describe('#load', () => {
it('retry 1 time', (done) => {
connector.load(['en'], ['retry1'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry1')).to.eql({
status: 'nok',
retries: 1,
});
done();
});
}).timeout(10850);

it('retry 5 times', (done) => {
connector.load(['en'], ['retry5'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry5')).to.eql({
status: 'nok',
retries: 5,
});
done();
});
}).timeout(10850 + 250); // ((2^5) - 1) * 350 = 10850

it('retry 6 times', (done) => {
connector.load(['en'], ['retry6'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry6')).to.eql({
status: 'nok',
retries: 6,
});
done();
});
}).timeout(22050 + 250); // ((2^6) - 1) * 350 = 22050

it('fail after retrying 6 times', (done) => {
connector.load(['en'], ['retry7'], function (err) {
expect(err).to.eql(['failed loading']);
expect(connector.store.getResourceBundle('en', 'retry7')).to.eql({});
done();
});
}).timeout(22050 + 250); // ((2^6) - 1) * 350 = 22050
});
});

// All tests have 250ms of code-exection buffer time built in.
// To ensure test correctness, the tests should never time out.
describe('BackendConnector retry with shorter intervals', () => {
let connector;

before(() => {
connector = new BackendConnector(
new BackendMock(),
new ResourceStore(),
{
interpolator: new Interpolator(),
},
{
retryTimeout: 100,
backend: { loadPath: 'http://localhost:9876/locales/{{lng}}/{{ns}}.json' },
},
);
});

describe('#load', () => {
it('retry 1 time', (done) => {
connector.load(['en'], ['retry1'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry1')).to.eql({
status: 'nok',
retries: 1,
});
done();
});
}).timeout(100 + 250);

it('retry 5 times', (done) => {
connector.load(['en'], ['retry5'], function (err) {
expect(connector.store.getResourceBundle('en', 'retry5')).to.eql({
status: 'nok',
retries: 5,
});
done();
});
}).timeout(3100 + 250); // ((2^5) - 1) * 100 = 3100
});
});

describe('BackendConnector retry with default maxRetries=0', () => {
let connector;

before(() => {
connector = new BackendConnector(
new BackendMock(),
new ResourceStore(),
{
interpolator: new Interpolator(),
},
{
// retryTimeout: 350, // This is the default value
maxRetries: 0, // This is the default value
backend: { loadPath: 'http://localhost:9876/locales/{{lng}}/{{ns}}.json' },
},
);
});

describe('#load', () => {
it('succeeds', (done) => {
connector.load(['en'], ['concurrently'], function (err) {
expect(err).to.eql(undefined);
expect(connector.store.getResourceBundle('en', 'concurrently')).to.eql({
status: 'ok',
namespace: 'concurrently',
});
done();
});
}).timeout(10850);

it('does not retry', (done) => {
connector.load(['en'], ['retry0'], function (err) {
expect(err).to.eql(['failed loading']);
expect(connector.store.getResourceBundle('en', 'retry0')).to.eql({});
done();
});
}).timeout(10850);
});
});
24 changes: 22 additions & 2 deletions test/backend/backendMock.js
Expand Up @@ -14,9 +14,29 @@ class Backend {

if (namespace.indexOf('fail') === 0) {
return callback('failed loading', true);
} else if (namespace === 'retry' && this.retries[language] < 2) {
} else if (namespace === 'retry0') {
this.retries[language]++;
return callback('failed loading', true);
} else if (namespace === 'retry1' && this.retries[language] < 1) {
this.retries[language]++;
return callback('failed loading', true);
} else if (namespace === 'retry2' && this.retries[language] < 2) {
this.retries[language]++;
return callback('failed loading', true);
} else if (namespace === 'retry5' && this.retries[language] < 5) {
this.retries[language]++;
return callback('failed loading', true);
} else if (namespace === 'retry6' && this.retries[language] < 6) {
this.retries[language]++;
return callback('failed loading', true);
} else if (namespace === 'retry7' && this.retries[language] < 7) {
this.retries[language]++;
return callback('failed loading', true);

// // Is a retry, but not set to fail after a specific
// } else if (namespace.indexOf('retry') === 0) {

// }
} else if (namespace.indexOf('concurrentlyLonger') === 0) {
setTimeout(() => {
callback(null, { status: 'ok', namespace });
Expand All @@ -40,7 +60,7 @@ class Backend {

if (!this.retries[language]) this.retries[language] = 0;

if (namespace === 'retry' && this.retries[language] < 2) {
if (namespace === 'retry2' && this.retries[language] < 2) {
this.retries[language]++;
return callback('failed loading', true);
} else {
Expand Down

0 comments on commit 655e985

Please sign in to comment.