Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make calculateDelay promisable #1266

Merged
merged 10 commits into from
May 16, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@ This also accepts an `object` with the following fields to constrain the duratio
Type: `number | object`\
Default:
- limit: `2`
- calculateDelay: `({attemptCount, retryOptions, error, computedValue}) => computedValue`
- calculateDelay: `({attemptCount, retryOptions, error, computedValue}) => computedValue | Promise<computedValue>`
- methods: `GET` `PUT` `HEAD` `DELETE` `OPTIONS` `TRACE`
- statusCodes: [`408`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) [`413`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413) [`429`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) [`500`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) [`502`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502) [`503`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503) [`504`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504) [`521`](https://support.cloudflare.com/hc/en-us/articles/115003011431#521error) [`522`](https://support.cloudflare.com/hc/en-us/articles/115003011431#522error) [`524`](https://support.cloudflare.com/hc/en-us/articles/115003011431#524error)
- maxRetryAfter: `undefined`
Expand All @@ -427,7 +427,7 @@ If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Ret

Delays between retries counts with function `1000 * Math.pow(2, retry) + Math.random() * 100`, where `retry` is attempt number (starts from 1).

The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value. The function must return a delay in milliseconds (`0` return value cancels retry).
The `calculateDelay` property is a `function` that receives an object with `attemptCount`, `retryOptions`, `error` and `computedValue` properties for current retry count, the retry options, error and default computed value. The function must return a delay in milliseconds (or a Promise resolving with it) (`0` return value cancels retry).

By default, it retries *only* on the specified methods, status codes, and on these network errors:
- `ETIMEDOUT`: One of the [timeout](#timeout) limits were reached.
Expand Down
21 changes: 12 additions & 9 deletions source/as-promise/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from './types';
import PromisableRequest, {parseBody} from './core';
import proxyEvents from '../core/utils/proxy-events';
import EventListenerReorderer from './utils/event-listeners-reorderer';

const proxiedRequestEvents = [
'request',
Expand Down Expand Up @@ -58,7 +59,9 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ

globalRequest = request;

request.once('response', async (response: Response) => {
const eventListenersReorderer = new EventListenerReorderer();

request.once('response', eventListenersReorderer.secondWrapper(async (response: Response) => {
response.retryCount = retryCount;

if (response.request.aborted) {
Expand Down Expand Up @@ -146,9 +149,9 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
globalResponse = response;

resolve(options.resolveBodyOnly ? response.body as T : response as unknown as T);
});
}));

request.once('error', (error: RequestError) => {
request.once('error', eventListenersReorderer.firstWrapper(async (error: RequestError) => {
if (promise.isCanceled) {
return;
}
Expand All @@ -163,7 +166,7 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
retryCount++;

try {
backoff = options.retry.calculateDelay({
backoff = await options.retry.calculateDelay({
attemptCount: retryCount,
retryOptions: options.retry,
error,
Expand All @@ -176,15 +179,15 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
});
} catch (error_) {
// Don't emit the `response` event
request.destroy();
eventListenersReorderer.ignoreSecond();

reject(new RequestError(error_.message, error, request));
return;
}

if (backoff) {
// Don't emit the `response` event
request.destroy();
eventListenersReorderer.ignoreSecond();

const retry = async (): Promise<void> => {
options.throwHttpErrors = throwHttpErrors;
Expand All @@ -196,7 +199,7 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
}
} catch (error_) {
// Don't emit the `response` event
request.destroy();
eventListenersReorderer.ignoreSecond();

reject(new RequestError(error_.message, error, request));
return;
Expand All @@ -218,10 +221,10 @@ export default function asPromise<T>(options: NormalizedOptions): CancelableRequ
}

// Don't emit the `response` event
request.destroy();
eventListenersReorderer.ignoreSecond();

reject(error);
});
}));

proxyEvents(request, emitter, proxiedRequestEvents);
};
Expand Down
3 changes: 2 additions & 1 deletion source/as-promise/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ export interface RetryObject {
}

export type RetryFunction = (retryObject: RetryObject) => number;
export type PromisableRetryFunction = (retryObject: RetryObject) => number | Promise<number>;

export interface RequiredRetryOptions {
limit: number;
methods: Method[];
statusCodes: number[];
errorCodes: string[];
calculateDelay: RetryFunction;
calculateDelay: PromisableRetryFunction;
maxRetryAfter?: number;
}

Expand Down
60 changes: 60 additions & 0 deletions source/as-promise/utils/event-listeners-reorderer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export default class EventListenerReorderer {
Giotino marked this conversation as resolved.
Show resolved Hide resolved
firstCalled = false;
Giotino marked this conversation as resolved.
Show resolved Hide resolved
waitPromise: Promise<any>;
waitResolve: any;
Giotino marked this conversation as resolved.
Show resolved Hide resolved
waitCalled = false;
Giotino marked this conversation as resolved.
Show resolved Hide resolved
_ignoreSecond = false;

constructor() {
this.waitPromise = new Promise(resolve => {
this.waitResolve = resolve;
});
}

async callBarrier() {
if (this.waitCalled) {
this.waitResolve();
}

this.waitCalled = true;

return this.waitPromise;
}

ignoreSecond() {
this._ignoreSecond = true;
}

firstWrapper(fn: (...p: any[]) => Promise<any>) {
return (...parameters: any[]) => {
this.firstCalled = true;
(async () => {
await fn(...parameters);

this.callBarrier();
})();
};
}

secondWrapper(fn: (...p: any[]) => Promise<void>) {
return (...parameters: any[]) => {
if (this._ignoreSecond) {
return;
}

if (this.firstCalled) {
(async () => {
await this.callBarrier();

if (this._ignoreSecond) {
return;
}

await fn(...parameters);
})();
} else {
fn(...parameters);
}
};
}
}
5 changes: 5 additions & 0 deletions source/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,11 @@ export default class Request extends Duplex implements RequestEvents<Request> {
return;
}

if (this[kRequest]!.destroyed) {
callback();
return;
}

this[kRequest]!.end((error?: Error | null) => {
if (!error) {
this[kBodySize] = this[kUploadedSize];
Expand Down
38 changes: 36 additions & 2 deletions test/retry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,43 @@ test('custom retries', withServer, async (t, server, got) => {
}

return 0;
}, methods: [
},
methods: [
'GET'
],
statusCodes: [
500
]
}
}));
t.is(error.response.statusCode, 500);
t.true(hasTried);
});

test('custom retries async', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 500;
response.end();
});

let hasTried = false;
const error = await t.throwsAsync<HTTPError>(got({
throwHttpErrors: true,
retry: {
calculateDelay: async ({attemptCount}) => {
/* eslint-disable-next-line promise/param-names */
await new Promise((resolve, _) => setTimeout(resolve, 1000));
if (attemptCount === 1) {
hasTried = true;
return 1;
}

return 0;
},
methods: [
'GET'
], statusCodes: [
],
statusCodes: [
500
]
}
Expand Down