-
Notifications
You must be signed in to change notification settings - Fork 224
/
Copy pathretryableRequest.ts
156 lines (138 loc) · 4.93 KB
/
retryableRequest.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { MethodEnum, Response } from '@algolia/requester-common';
import {
createRetryError,
createStatefulHost,
deserializeFailure,
deserializeSuccess,
HostStatusEnum,
MappedRequestOptions,
Request,
serializeData,
serializeHeaders,
serializeUrl,
StackFrame,
stackFrameWithoutCredentials,
stackTraceWithoutCredentials,
StatelessHost,
Transporter,
} from '..';
import { createRetryableOptions } from './createRetryableOptions';
import { Outcomes, retryDecision } from './retryDecision';
export function retryableRequest<TResponse>(
transporter: Transporter,
statelessHosts: readonly StatelessHost[],
request: Request,
requestOptions: MappedRequestOptions
): Readonly<Promise<TResponse>> {
const stackTrace: StackFrame[] = []; // eslint-disable-line functional/prefer-readonly-type
/**
* First we prepare the payload that do not depend from hosts.
*/
const data = serializeData(transporter, request, requestOptions);
const headers = serializeHeaders(transporter, requestOptions);
const method = request.method;
// On `GET`, the data is proxied to query parameters.
const dataQueryParameters: Record<string, any> =
request.method !== MethodEnum.Get
? {}
: {
// if AuthMode.WithinData, we forcibly map apiKey back to a query param
'x-algolia-api-key': transporter.data?.apiKey,
...request.data,
...requestOptions.data,
};
const queryParameters = {
'x-algolia-agent': transporter.userAgent.value,
...transporter.queryParameters,
...dataQueryParameters,
...requestOptions.queryParameters,
};
let timeoutsCount = 0; // eslint-disable-line functional/no-let
const retry = (
hosts: StatelessHost[], // eslint-disable-line functional/prefer-readonly-type
getTimeout: (timeoutsCount: number, timeout: number) => number
): Readonly<Promise<TResponse>> => {
/**
* We iterate on each host, until there is no host left.
*/
const host = hosts.pop(); // eslint-disable-line functional/immutable-data
if (host === undefined) {
throw createRetryError(stackTraceWithoutCredentials(stackTrace));
}
const payload = {
data,
headers,
method,
url: serializeUrl(host, request.path, queryParameters),
connectTimeout: getTimeout(timeoutsCount, transporter.timeouts.connect),
responseTimeout: getTimeout(timeoutsCount, requestOptions.timeout as number),
};
/**
* The stackFrame is pushed to the stackTrace so we
* can have information about onRetry and onFailure
* decisions.
*/
const pushToStackTrace = (response: Response): StackFrame => {
const stackFrame: StackFrame = {
request: payload,
response,
host,
triesLeft: hosts.length,
};
// eslint-disable-next-line functional/immutable-data
stackTrace.push(stackFrame);
return stackFrame;
};
const decisions: Outcomes<TResponse> = {
onSuccess: response => deserializeSuccess(response),
onRetry(response) {
const stackFrame = pushToStackTrace(response);
/**
* If response is a timeout, we increaset the number of
* timeouts so we can increase the timeout later.
*/
if (response.isTimedOut) {
timeoutsCount++;
}
return Promise.all([
/**
* Failures are individually send the logger, allowing
* the end user to debug / store stack frames even
* when a retry error does not happen.
*/
transporter.logger.info('Retryable failure', stackFrameWithoutCredentials(stackFrame)),
/**
* We also store the state of the host in failure cases. If the host, is
* down it will remain down for the next 2 minutes. In a timeout situation,
* this host will be added end of the list of hosts on the next request.
*/
transporter.hostsCache.set(
host,
createStatefulHost(
host,
response.isTimedOut ? HostStatusEnum.Timeouted : HostStatusEnum.Down
)
),
]).then(() => retry(hosts, getTimeout));
},
onFail(response) {
pushToStackTrace(response);
throw deserializeFailure(response, stackTraceWithoutCredentials(stackTrace));
},
};
return transporter.requester.send(payload).then(response => {
return retryDecision(response, decisions);
});
};
/**
* Finally, for each retryable host perform request until we got a non
* retryable response. Some notes here:
*
* 1. The reverse here is applied so we can apply a `pop` later on => more performant.
* 2. We also get from the retryable options a timeout multiplier that is tailored
* for the current context.
*/
return createRetryableOptions(transporter.hostsCache, statelessHosts).then(options => {
return retry([...options.statelessHosts].reverse(), options.getTimeout);
});
}