UNPKG

6.59 kBPlain TextView Raw
1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License. See License.txt in the project root for license information.
3
4import { HttpOperationResponse } from "../httpOperationResponse";
5import * as utils from "../util/utils";
6import { WebResourceLike } from "../webResource";
7import {
8 BaseRequestPolicy,
9 RequestPolicy,
10 RequestPolicyFactory,
11 RequestPolicyOptionsLike,
12} from "./requestPolicy";
13import { RestError } from "../restError";
14
15export interface RetryData {
16 retryCount: number;
17 retryInterval: number;
18 error?: RetryError;
19}
20
21export interface RetryError extends Error {
22 message: string;
23 code?: string;
24 innerError?: RetryError;
25}
26
27export function exponentialRetryPolicy(
28 retryCount?: number,
29 retryInterval?: number,
30 minRetryInterval?: number,
31 maxRetryInterval?: number
32): RequestPolicyFactory {
33 return {
34 create: (nextPolicy: RequestPolicy, options: RequestPolicyOptionsLike) => {
35 return new ExponentialRetryPolicy(
36 nextPolicy,
37 options,
38 retryCount,
39 retryInterval,
40 minRetryInterval,
41 maxRetryInterval
42 );
43 },
44 };
45}
46
47const DEFAULT_CLIENT_RETRY_INTERVAL = 1000 * 30;
48const DEFAULT_CLIENT_RETRY_COUNT = 3;
49const DEFAULT_CLIENT_MAX_RETRY_INTERVAL = 1000 * 90;
50const DEFAULT_CLIENT_MIN_RETRY_INTERVAL = 1000 * 3;
51
52/**
53 * @class
54 * Instantiates a new "ExponentialRetryPolicyFilter" instance.
55 */
56export class ExponentialRetryPolicy extends BaseRequestPolicy {
57 /**
58 * The client retry count.
59 */
60 retryCount: number;
61 /**
62 * The client retry interval in milliseconds.
63 */
64 retryInterval: number;
65 /**
66 * The minimum retry interval in milliseconds.
67 */
68 minRetryInterval: number;
69 /**
70 * The maximum retry interval in milliseconds.
71 */
72 maxRetryInterval: number;
73
74 /**
75 * @constructor
76 * @param {RequestPolicy} nextPolicy The next RequestPolicy in the pipeline chain.
77 * @param {RequestPolicyOptionsLike} options The options for this RequestPolicy.
78 * @param {number} [retryCount] The client retry count.
79 * @param {number} [retryInterval] The client retry interval, in milliseconds.
80 * @param {number} [minRetryInterval] The minimum retry interval, in milliseconds.
81 * @param {number} [maxRetryInterval] The maximum retry interval, in milliseconds.
82 */
83 constructor(
84 nextPolicy: RequestPolicy,
85 options: RequestPolicyOptionsLike,
86 retryCount?: number,
87 retryInterval?: number,
88 minRetryInterval?: number,
89 maxRetryInterval?: number
90 ) {
91 super(nextPolicy, options);
92 function isNumber(n: any): n is number {
93 return typeof n === "number";
94 }
95 this.retryCount = isNumber(retryCount) ? retryCount : DEFAULT_CLIENT_RETRY_COUNT;
96 this.retryInterval = isNumber(retryInterval) ? retryInterval : DEFAULT_CLIENT_RETRY_INTERVAL;
97 this.minRetryInterval = isNumber(minRetryInterval)
98 ? minRetryInterval
99 : DEFAULT_CLIENT_MIN_RETRY_INTERVAL;
100 this.maxRetryInterval = isNumber(maxRetryInterval)
101 ? maxRetryInterval
102 : DEFAULT_CLIENT_MAX_RETRY_INTERVAL;
103 }
104
105 public sendRequest(request: WebResourceLike): Promise<HttpOperationResponse> {
106 return this._nextPolicy
107 .sendRequest(request.clone())
108 .then((response) => retry(this, request, response))
109 .catch((error) => retry(this, request, error.response, undefined, error));
110 }
111}
112
113/**
114 * Determines if the operation should be retried and how long to wait until the next retry.
115 *
116 * @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
117 * @param {number} statusCode The HTTP status code.
118 * @param {RetryData} retryData The retry data.
119 * @return {boolean} True if the operation qualifies for a retry; false otherwise.
120 */
121function shouldRetry(
122 policy: ExponentialRetryPolicy,
123 statusCode: number | undefined,
124 retryData: RetryData
125): boolean {
126 if (
127 statusCode == undefined ||
128 (statusCode < 500 && statusCode !== 408) ||
129 statusCode === 501 ||
130 statusCode === 505
131 ) {
132 return false;
133 }
134
135 let currentCount: number;
136 if (!retryData) {
137 throw new Error("retryData for the ExponentialRetryPolicyFilter cannot be null.");
138 } else {
139 currentCount = retryData && retryData.retryCount;
140 }
141
142 return currentCount < policy.retryCount;
143}
144
145/**
146 * Updates the retry data for the next attempt.
147 *
148 * @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
149 * @param {RetryData} retryData The retry data.
150 * @param {RetryError} [err] The operation"s error, if any.
151 */
152function updateRetryData(
153 policy: ExponentialRetryPolicy,
154 retryData?: RetryData,
155 err?: RetryError
156): RetryData {
157 if (!retryData) {
158 retryData = {
159 retryCount: 0,
160 retryInterval: 0,
161 };
162 }
163
164 if (err) {
165 if (retryData.error) {
166 err.innerError = retryData.error;
167 }
168
169 retryData.error = err;
170 }
171
172 // Adjust retry count
173 retryData.retryCount++;
174
175 // Adjust retry interval
176 let incrementDelta = Math.pow(2, retryData.retryCount) - 1;
177 const boundedRandDelta =
178 policy.retryInterval * 0.8 +
179 Math.floor(Math.random() * (policy.retryInterval * 1.2 - policy.retryInterval * 0.8));
180 incrementDelta *= boundedRandDelta;
181
182 retryData.retryInterval = Math.min(
183 policy.minRetryInterval + incrementDelta,
184 policy.maxRetryInterval
185 );
186
187 return retryData;
188}
189
190function retry(
191 policy: ExponentialRetryPolicy,
192 request: WebResourceLike,
193 response?: HttpOperationResponse,
194 retryData?: RetryData,
195 requestError?: RetryError
196): Promise<HttpOperationResponse> {
197 retryData = updateRetryData(policy, retryData, requestError);
198 const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted;
199 if (!isAborted && shouldRetry(policy, response && response.status, retryData)) {
200 return utils
201 .delay(retryData.retryInterval)
202 .then(() => policy._nextPolicy.sendRequest(request.clone()))
203 .then((res) => retry(policy, request, res, retryData, undefined))
204 .catch((err) => retry(policy, request, response, retryData, err));
205 } else if (isAborted || requestError || !response) {
206 // If the operation failed in the end, return all errors instead of just the last one
207 const err =
208 retryData.error ||
209 new RestError(
210 "Failed to send the request.",
211 RestError.REQUEST_SEND_ERROR,
212 response && response.status,
213 response && response.request,
214 response
215 );
216 return Promise.reject(err);
217 } else {
218 return Promise.resolve(response);
219 }
220}