Skip to content

Commit

Permalink
feat(client): add support for global retry configuration (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
pablopalacios authored May 6, 2022
1 parent 949a798 commit cd21c04
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 50 deletions.
74 changes: 41 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,58 +433,66 @@ For requests from the server, the config object is simply passed into the servic

## Retry

You can set Fetchr to retry failed requests automatically by setting a `retry` settings in the client configuration:
You can set Fetchr to automatically retry failed requests by specifying a `retry` configuration in the global or in the request configuration:

```js
fetcher
// Globally
const fetchr = new Fetchr({
retry: { maxRetries: 2 },
});

// Per request
fetchr
.read('service')
.clientConfig({
retry: {
maxRetries: 2,
},
retry: { maxRetries: 1 },
})
.end();
```

With this configuration, Fetchr will retry all requests that fail with 408 status code or that failed without even reaching the service (status code 0 means, for example, that the client was not able to reach the server) two more times before returning an error. The interval between each request respects
the following formula, based on the exponential backoff and full jitter strategy published in [this AWS architecture blog post](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/):
With the above configuration, Fetchr will retry twice all requests
that fail but only once when calling `read('service')`.

You can further customize how the retry mechanism works. These are all
settings and their default values:

```js
Math.random() * Math.pow(2, attempt) * interval;
const fetchr = new Fetchr({
retry: {
maxRetries: 2, // amount of retries after the first failed request
interval: 200, // maximum interval between each request in ms (see note below)
statusCodes: [0, 408], // response status code that triggers a retry (see note below)
},
unsafeAllowRetry: false, // allow unsafe operations to be retried (see note below)
}
```
`attempt` is the number of the current retry attempt starting
from 0. By default `interval` corresponds to 200ms.
**interval**
You can customize the retry behavior by adding more properties in the
`retry` object:
The interval between each request respects the following formula, based on the exponential backoff and full jitter strategy published in [this AWS architecture blog post](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/):
```js
fetcher
.read('resource')
.clientConfig({
retry: {
maxRetries: 5,
interval: 1000,
statusCodes: [408, 502],
},
})
.end();
Math.random() * Math.pow(2, attempt) * interval;
```
With the above configuration, Fetchr will retry all failed (408 or 502 status code) requests for a maximum of 5 times. The interval between each request will still use the formula from above, but the interval of 1000ms will be used instead.
`attempt` is the number of the current retry attempt starting
from 0. By default `interval` corresponds to 200ms.
**Note:** Fetchr doesn't retry POST requests for safety reasons. You can enable retries for POST requests by setting the `unsafeAllowRetry` property to `true`:
**statusCodes**
```js
fetcher
.create('resource')
.clientConfig({
retry: { maxRetries: 2 },
unsafeAllowRetry: true,
})
.end();
```
For historical reasons, fetchr only retries 408 responses and no
responses at all (for example, a network error, indicated by a status
code 0). However, you might find useful to also retry on other codes
as well (502, 503, 504 can be good candidates for an automatic
retries).
**unsafeAllowRetry**
By default, Fetchr only retries `read` requests. This is done for
safety reasons: reading twice an entry from a database is not as bad
as creating an entry twice. But if your application or resource
doesn't need this kind of protection, you can allow retries by setting
`unsafeAllowRetry` to `true` and fetchr will retry all operations.
## Context Variables
Expand Down
2 changes: 2 additions & 0 deletions libs/fetcher.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ function Fetcher(options) {
corsPath: options.corsPath,
context: options.context || {},
contextPicker: options.contextPicker || {},
retry: options.retry || null,
statsCollector: options.statsCollector,
unsafeAllowRetry: Boolean(options.unsafeAllowRetry),
_serviceMeta: this._serviceMeta,
};
}
Expand Down
33 changes: 16 additions & 17 deletions libs/util/normalizeOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ function requestToOptions(request) {

var config = Object.assign(
{
unsafeAllowRetry: request.operation === 'read',
xhrTimeout: request.options.xhrTimeout,
},
request._clientConfig
Expand Down Expand Up @@ -83,24 +82,24 @@ function normalizeHeaders(options) {
return headers;
}

function normalizeRetry(options) {
var retry = {
interval: 200,
maxRetries: 0,
retryOnPost: false,
statusCodes: [0, 408, 999],
};

if (!options.config.retry) {
return retry;
}
function normalizeRetry(request) {
var retry = Object.assign(
{
interval: 200,
maxRetries: 0,
retryOnPost:
request.operation === 'read' ||
request.options.unsafeAllowRetry,
statusCodes: [0, 408, 999],
},
request.options.retry,
request._clientConfig.retry
);

if (options.config.unsafeAllowRetry) {
retry.retryOnPost = true;
if ('unsafeAllowRetry' in request._clientConfig) {
retry.retryOnPost = request._clientConfig.unsafeAllowRetry;
}

Object.assign(retry, options.config.retry);

if (retry.max_retries) {
console.warn(
'"max_retries" is deprecated and will be removed in a future release, use "maxRetries" instead.'
Expand All @@ -118,7 +117,7 @@ function normalizeOptions(request) {
body: options.data != null ? JSON.stringify(options.data) : undefined,
headers: normalizeHeaders(options),
method: options.method,
retry: normalizeRetry(options),
retry: normalizeRetry(request),
timeout: options.config.timeout || options.config.xhrTimeout,
url: options.url,
};
Expand Down
79 changes: 79 additions & 0 deletions tests/unit/libs/fetcher.client.js
Original file line number Diff line number Diff line change
Expand Up @@ -649,4 +649,83 @@ describe('Client Fetcher', function () {
});
});
});

describe('Custom retry', function () {
describe('should be configurable globally', function () {
before(function () {
mockery.registerMock('./util/httpRequest', function (options) {
expect(options.retry).to.deep.equal({
interval: 350,
maxRetries: 2,
retryOnPost: true,
statusCodes: [0, 502, 504],
});
return httpRequest(options);
});
mockery.enable({
useCleanCache: true,
warnOnUnregistered: false,
});

Fetcher = require('../../../libs/fetcher.client');

this.fetcher = new Fetcher({
retry: {
interval: 350,
maxRetries: 2,
statusCodes: [0, 502, 504],
},
unsafeAllowRetry: true,
});
});

testCrud(params, body, config, callback, resolve, reject);

after(function () {
mockery.deregisterMock('./util/httpRequest');
mockery.disable();
});
});

describe('should be configurable per request', function () {
before(function () {
mockery.registerMock('./util/httpRequest', function (options) {
expect(options.retry).to.deep.equal({
interval: 350,
maxRetries: 2,
retryOnPost: true,
statusCodes: [0, 502, 504],
});
return httpRequest(options);
});
mockery.enable({
useCleanCache: true,
warnOnUnregistered: false,
});
Fetcher = require('../../../libs/fetcher.client');
this.fetcher = new Fetcher({});
});
var customConfig = {
retry: {
interval: 350,
maxRetries: 2,
statusCodes: [0, 502, 504],
},
unsafeAllowRetry: true,
};
testCrud({
disableNoConfigTests: true,
params: params,
body: body,
config: customConfig,
callback: callback,
resolve: resolve,
reject: reject,
});
after(function () {
mockery.deregisterMock('./util/httpRequest');
mockery.disable();
});
});
});
});

0 comments on commit cd21c04

Please sign in to comment.