Source: lib/net/backoff.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.net.Backoff');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.util.PublicPromise');
  20. /**
  21. * Backoff represents delay and backoff state. This is used by NetworkingEngine
  22. * for individual requests and by StreamingEngine to retry streaming failures.
  23. *
  24. * @param {shakaExtern.RetryParameters} parameters
  25. * @param {boolean=} opt_autoReset If true, start at a "first retry" state and
  26. * and auto-reset that state when we reach maxAttempts.
  27. * @param {?function()=} opt_isCanceled If provided, the backoff will end the
  28. * current attempt early when this callback returns true.
  29. *
  30. * @struct
  31. * @constructor
  32. */
  33. shaka.net.Backoff = function(parameters, opt_autoReset, opt_isCanceled) {
  34. // Set defaults as we unpack these, so that individual app-level requests in
  35. // NetworkingEngine can be missing parameters.
  36. let defaults = shaka.net.Backoff.defaultRetryParameters();
  37. /**
  38. * @const
  39. * @private {number}
  40. */
  41. this.maxAttempts_ = (parameters.maxAttempts == null) ?
  42. defaults.maxAttempts : parameters.maxAttempts;
  43. goog.asserts.assert(this.maxAttempts_ >= 1, 'maxAttempts should be >= 1');
  44. /**
  45. * @const
  46. * @private {number}
  47. */
  48. this.baseDelay_ = (parameters.baseDelay == null) ?
  49. defaults.baseDelay : parameters.baseDelay;
  50. goog.asserts.assert(this.baseDelay_ >= 0, 'baseDelay should be >= 0');
  51. /**
  52. * @const
  53. * @private {number}
  54. */
  55. this.fuzzFactor_ = (parameters.fuzzFactor == null) ?
  56. defaults.fuzzFactor : parameters.fuzzFactor;
  57. goog.asserts.assert(this.fuzzFactor_ >= 0, 'fuzzFactor should be >= 0');
  58. /**
  59. * @const
  60. * @private {number}
  61. */
  62. this.backoffFactor_ = (parameters.backoffFactor == null) ?
  63. defaults.backoffFactor : parameters.backoffFactor;
  64. goog.asserts.assert(this.backoffFactor_ >= 0, 'backoffFactor should be >= 0');
  65. /** @private {number} */
  66. this.numAttempts_ = 0;
  67. /** @private {number} */
  68. this.nextUnfuzzedDelay_ = this.baseDelay_;
  69. /** @private {boolean} */
  70. this.autoReset_ = opt_autoReset || false;
  71. /** @private {?function()} */
  72. this.isCanceled_ = opt_isCanceled || null;
  73. if (this.autoReset_) {
  74. // There is no delay before the first attempt. In StreamingEngine (the
  75. // intended user of auto-reset mode), the first attempt was implied, so we
  76. // reset numAttempts to 1. Therefore maxAttempts (which includes the first
  77. // attempt) must be at least 2 for us to see a delay.
  78. goog.asserts.assert(this.maxAttempts_ >= 2,
  79. 'maxAttempts must be >= 2 for autoReset == true');
  80. this.numAttempts_ = 1;
  81. }
  82. };
  83. /**
  84. * @return {!Promise} Resolves when the caller may make an attempt, possibly
  85. * after a delay. Rejects if no more attempts are allowed.
  86. */
  87. shaka.net.Backoff.prototype.attempt = function() {
  88. if (this.numAttempts_ >= this.maxAttempts_) {
  89. if (this.autoReset_) {
  90. this.reset_();
  91. } else {
  92. return Promise.reject();
  93. }
  94. }
  95. let p = new shaka.util.PublicPromise();
  96. if (this.numAttempts_) {
  97. // We've already tried before, so delay the Promise.
  98. // Fuzz the delay to avoid tons of clients hitting the server at once
  99. // after it recovers from whatever is causing it to fail.
  100. let fuzzedDelay =
  101. shaka.net.Backoff.fuzz_(this.nextUnfuzzedDelay_, this.fuzzFactor_);
  102. this.cancelableTimeout_(p.resolve, fuzzedDelay);
  103. // Update delay_ for next time.
  104. this.nextUnfuzzedDelay_ *= this.backoffFactor_;
  105. } else {
  106. goog.asserts.assert(!this.autoReset_, 'Failed to delay with auto-reset!');
  107. p.resolve();
  108. }
  109. this.numAttempts_++;
  110. return p;
  111. };
  112. /**
  113. * Gets a copy of the default retry parameters.
  114. *
  115. * @return {shakaExtern.RetryParameters}
  116. */
  117. shaka.net.Backoff.defaultRetryParameters = function() {
  118. // Use a function rather than a constant member so the calling code can
  119. // modify the values without affecting other call results.
  120. return {
  121. maxAttempts: 2,
  122. baseDelay: 1000,
  123. backoffFactor: 2,
  124. fuzzFactor: 0.5,
  125. timeout: 0
  126. };
  127. };
  128. /**
  129. * Fuzz the input value by +/- fuzzFactor. For example, a fuzzFactor of 0.5
  130. * will create a random value that is between 50% and 150% of the input value.
  131. *
  132. * @param {number} value
  133. * @param {number} fuzzFactor
  134. * @return {number} The fuzzed value
  135. * @private
  136. */
  137. shaka.net.Backoff.fuzz_ = function(value, fuzzFactor) {
  138. // A random number between -1 and +1.
  139. let negToPosOne = (Math.random() * 2.0) - 1.0;
  140. // A random number between -fuzzFactor and +fuzzFactor.
  141. let negToPosFuzzFactor = negToPosOne * fuzzFactor;
  142. // The original value, fuzzed by +/- fuzzFactor.
  143. return value * (1.0 + negToPosFuzzFactor);
  144. };
  145. /**
  146. * Reset state in autoReset mode.
  147. * @private
  148. */
  149. shaka.net.Backoff.prototype.reset_ = function() {
  150. goog.asserts.assert(this.autoReset_, 'Should only be used for auto-reset!');
  151. this.numAttempts_ = 1;
  152. this.nextUnfuzzedDelay_ = this.baseDelay_;
  153. };
  154. /**
  155. * Makes a timeout that cancels with isCanceled_ if this has an isCanceled_.
  156. *
  157. * @param {Function} fn The callback to invoke when the timeout expires.
  158. * @param {number} timeoutMs The timeout in milliseconds.
  159. * @private
  160. */
  161. shaka.net.Backoff.prototype.cancelableTimeout_ = function(fn, timeoutMs) {
  162. if (this.isCanceled_) {
  163. if (this.isCanceled_() || timeoutMs == 0) {
  164. fn();
  165. } else {
  166. // This will break the timeout into 200 ms intervals, so that isCanceled_
  167. // will be checked periodically.
  168. let timeToUse = Math.min(200, timeoutMs);
  169. shaka.net.Backoff.setTimeout_(function() {
  170. this.cancelableTimeout_(fn, timeoutMs - timeToUse);
  171. }.bind(this), timeToUse);
  172. }
  173. } else {
  174. shaka.net.Backoff.setTimeout_(fn, timeoutMs);
  175. }
  176. };
  177. /**
  178. * This is here only for testability. Mocking global setTimeout can lead to
  179. * unintended interactions with other tests. So instead, we mock this.
  180. *
  181. * @param {Function} fn The callback to invoke when the timeout expires.
  182. * @param {number} timeoutMs The timeout in milliseconds.
  183. * @return {number} The timeout ID.
  184. * @private
  185. */
  186. shaka.net.Backoff.setTimeout_ = function(fn, timeoutMs) {
  187. return window.setTimeout(fn, timeoutMs);
  188. };