exponentialRetryPolicy.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  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. import { HttpOperationResponse } from "../httpOperationResponse";
  4. import * as utils from "../util/utils";
  5. import { WebResource } from "../webResource";
  6. import { BaseRequestPolicy, RequestPolicy, RequestPolicyFactory, RequestPolicyOptions } from "./requestPolicy";
  7. import { RestError } from "../restError";
  8. export interface RetryData {
  9. retryCount: number;
  10. retryInterval: number;
  11. error?: RetryError;
  12. }
  13. export interface RetryError extends Error {
  14. message: string;
  15. code?: string;
  16. innerError?: RetryError;
  17. }
  18. export function exponentialRetryPolicy(retryCount?: number, retryInterval?: number, minRetryInterval?: number, maxRetryInterval?: number): RequestPolicyFactory {
  19. return {
  20. create: (nextPolicy: RequestPolicy, options: RequestPolicyOptions) => {
  21. return new ExponentialRetryPolicy(nextPolicy, options, retryCount, retryInterval, minRetryInterval, maxRetryInterval);
  22. }
  23. };
  24. }
  25. const DEFAULT_CLIENT_RETRY_INTERVAL = 1000 * 30;
  26. const DEFAULT_CLIENT_RETRY_COUNT = 3;
  27. const DEFAULT_CLIENT_MAX_RETRY_INTERVAL = 1000 * 90;
  28. const DEFAULT_CLIENT_MIN_RETRY_INTERVAL = 1000 * 3;
  29. /**
  30. * @class
  31. * Instantiates a new "ExponentialRetryPolicyFilter" instance.
  32. */
  33. export class ExponentialRetryPolicy extends BaseRequestPolicy {
  34. /**
  35. * The client retry count.
  36. */
  37. retryCount: number;
  38. /**
  39. * The client retry interval in milliseconds.
  40. */
  41. retryInterval: number;
  42. /**
  43. * The minimum retry interval in milliseconds.
  44. */
  45. minRetryInterval: number;
  46. /**
  47. * The maximum retry interval in milliseconds.
  48. */
  49. maxRetryInterval: number;
  50. /**
  51. * @constructor
  52. * @param {RequestPolicy} nextPolicy The next RequestPolicy in the pipeline chain.
  53. * @param {RequestPolicyOptions} options The options for this RequestPolicy.
  54. * @param {number} [retryCount] The client retry count.
  55. * @param {number} [retryInterval] The client retry interval, in milliseconds.
  56. * @param {number} [minRetryInterval] The minimum retry interval, in milliseconds.
  57. * @param {number} [maxRetryInterval] The maximum retry interval, in milliseconds.
  58. */
  59. constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, retryCount?: number, retryInterval?: number, minRetryInterval?: number, maxRetryInterval?: number) {
  60. super(nextPolicy, options);
  61. function isNumber(n: any): n is number { return typeof n === "number"; }
  62. this.retryCount = isNumber(retryCount) ? retryCount : DEFAULT_CLIENT_RETRY_COUNT;
  63. this.retryInterval = isNumber(retryInterval) ? retryInterval : DEFAULT_CLIENT_RETRY_INTERVAL;
  64. this.minRetryInterval = isNumber(minRetryInterval) ? minRetryInterval : DEFAULT_CLIENT_MIN_RETRY_INTERVAL;
  65. this.maxRetryInterval = isNumber(maxRetryInterval) ? maxRetryInterval : DEFAULT_CLIENT_MAX_RETRY_INTERVAL;
  66. }
  67. public sendRequest(request: WebResource): Promise<HttpOperationResponse> {
  68. return this._nextPolicy.sendRequest(request.clone())
  69. .then(response => retry(this, request, response))
  70. .catch(error => retry(this, request, error.response, undefined, error));
  71. }
  72. }
  73. /**
  74. * Determines if the operation should be retried and how long to wait until the next retry.
  75. *
  76. * @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
  77. * @param {number} statusCode The HTTP status code.
  78. * @param {RetryData} retryData The retry data.
  79. * @return {boolean} True if the operation qualifies for a retry; false otherwise.
  80. */
  81. function shouldRetry(policy: ExponentialRetryPolicy, statusCode: number | undefined, retryData: RetryData): boolean {
  82. if (statusCode == undefined || (statusCode < 500 && statusCode !== 408) || statusCode === 501 || statusCode === 505) {
  83. return false;
  84. }
  85. let currentCount: number;
  86. if (!retryData) {
  87. throw new Error("retryData for the ExponentialRetryPolicyFilter cannot be null.");
  88. } else {
  89. currentCount = (retryData && retryData.retryCount);
  90. }
  91. return (currentCount < policy.retryCount);
  92. }
  93. /**
  94. * Updates the retry data for the next attempt.
  95. *
  96. * @param {ExponentialRetryPolicy} policy The ExponentialRetryPolicy that this function is being called against.
  97. * @param {RetryData} retryData The retry data.
  98. * @param {RetryError} [err] The operation"s error, if any.
  99. */
  100. function updateRetryData(policy: ExponentialRetryPolicy, retryData?: RetryData, err?: RetryError): RetryData {
  101. if (!retryData) {
  102. retryData = {
  103. retryCount: 0,
  104. retryInterval: 0
  105. };
  106. }
  107. if (err) {
  108. if (retryData.error) {
  109. err.innerError = retryData.error;
  110. }
  111. retryData.error = err;
  112. }
  113. // Adjust retry count
  114. retryData.retryCount++;
  115. // Adjust retry interval
  116. let incrementDelta = Math.pow(2, retryData.retryCount) - 1;
  117. const boundedRandDelta = policy.retryInterval * 0.8 +
  118. Math.floor(Math.random() * (policy.retryInterval * 1.2 - policy.retryInterval * 0.8));
  119. incrementDelta *= boundedRandDelta;
  120. retryData.retryInterval = Math.min(policy.minRetryInterval + incrementDelta, policy.maxRetryInterval);
  121. return retryData;
  122. }
  123. function retry(policy: ExponentialRetryPolicy, request: WebResource, response?: HttpOperationResponse, retryData?: RetryData, requestError?: RetryError): Promise<HttpOperationResponse> {
  124. retryData = updateRetryData(policy, retryData, requestError);
  125. const isAborted: boolean | undefined = request.abortSignal && request.abortSignal.aborted;
  126. if (!isAborted && shouldRetry(policy, response && response.status, retryData)) {
  127. return utils.delay(retryData.retryInterval)
  128. .then(() => policy._nextPolicy.sendRequest(request.clone()))
  129. .then(res => retry(policy, request, res, retryData, undefined))
  130. .catch(err => retry(policy, request, response, retryData, err));
  131. } else if (isAborted || requestError || !response) {
  132. // If the operation failed in the end, return all errors instead of just the last one
  133. const err = retryData.error ||
  134. new RestError(
  135. "Failed to send the request.",
  136. RestError.REQUEST_SEND_ERROR,
  137. response && response.status,
  138. response && response.request,
  139. response);
  140. return Promise.reject(err);
  141. } else {
  142. return Promise.resolve(response);
  143. }
  144. }