Source: lib/net/backoff.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.net.Backoff');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.util.Error');
  9. goog.require('shaka.util.Timer');
  10. /**
  11. * Backoff represents delay and backoff state. This is used by NetworkingEngine
  12. * for individual requests and by StreamingEngine to retry streaming failures.
  13. *
  14. * @final
  15. */
  16. shaka.net.Backoff = class {
  17. /**
  18. * @param {shaka.extern.RetryParameters} parameters
  19. * @param {boolean=} autoReset If true, start at a "first retry" state and
  20. * and auto-reset that state when we reach maxAttempts.
  21. * Default set to false.
  22. */
  23. constructor(parameters, autoReset = false) {
  24. // Set defaults as we unpack these, so that individual app-level requests in
  25. // NetworkingEngine can be missing parameters.
  26. const defaults = shaka.net.Backoff.defaultRetryParameters();
  27. /**
  28. * @const
  29. * @private {number}
  30. */
  31. this.maxAttempts_ = (parameters.maxAttempts == null) ?
  32. defaults.maxAttempts : parameters.maxAttempts;
  33. goog.asserts.assert(this.maxAttempts_ >= 1, 'maxAttempts should be >= 1');
  34. /**
  35. * @const
  36. * @private {number}
  37. */
  38. this.baseDelay_ = (parameters.baseDelay == null) ?
  39. defaults.baseDelay : parameters.baseDelay;
  40. goog.asserts.assert(this.baseDelay_ >= 0, 'baseDelay should be >= 0');
  41. /**
  42. * @const
  43. * @private {number}
  44. */
  45. this.fuzzFactor_ = (parameters.fuzzFactor == null) ?
  46. defaults.fuzzFactor : parameters.fuzzFactor;
  47. goog.asserts.assert(this.fuzzFactor_ >= 0, 'fuzzFactor should be >= 0');
  48. /**
  49. * @const
  50. * @private {number}
  51. */
  52. this.backoffFactor_ = (parameters.backoffFactor == null) ?
  53. defaults.backoffFactor : parameters.backoffFactor;
  54. goog.asserts.assert(
  55. this.backoffFactor_ >= 0, 'backoffFactor should be >= 0');
  56. /** @private {number} */
  57. this.numAttempts_ = 0;
  58. /** @private {number} */
  59. this.nextUnfuzzedDelay_ = this.baseDelay_;
  60. /** @private {boolean} */
  61. this.autoReset_ = autoReset;
  62. if (this.autoReset_) {
  63. // There is no delay before the first attempt. In StreamingEngine (the
  64. // intended user of auto-reset mode), the first attempt was implied, so we
  65. // reset numAttempts to 1. Therefore maxAttempts (which includes the
  66. // first attempt) must be at least 2 for us to see a delay.
  67. goog.asserts.assert(this.maxAttempts_ >= 2,
  68. 'maxAttempts must be >= 2 for autoReset == true');
  69. this.numAttempts_ = 1;
  70. }
  71. }
  72. /**
  73. * @return {!Promise} Resolves when the caller may make an attempt, possibly
  74. * after a delay. Rejects if no more attempts are allowed.
  75. */
  76. async attempt() {
  77. if (this.numAttempts_ >= this.maxAttempts_) {
  78. if (this.autoReset_) {
  79. this.reset_();
  80. } else {
  81. throw new shaka.util.Error(
  82. shaka.util.Error.Severity.CRITICAL,
  83. shaka.util.Error.Category.PLAYER,
  84. shaka.util.Error.Code.ATTEMPTS_EXHAUSTED);
  85. }
  86. }
  87. const currentAttempt = this.numAttempts_;
  88. this.numAttempts_++;
  89. if (currentAttempt == 0) {
  90. goog.asserts.assert(!this.autoReset_, 'Failed to delay with auto-reset!');
  91. return;
  92. }
  93. // We've already tried before, so delay the Promise.
  94. // Fuzz the delay to avoid tons of clients hitting the server at once
  95. // after it recovers from whatever is causing it to fail.
  96. const fuzzedDelayMs = shaka.net.Backoff.fuzz_(
  97. this.nextUnfuzzedDelay_, this.fuzzFactor_);
  98. await new Promise((resolve) => {
  99. shaka.net.Backoff.defer(fuzzedDelayMs, resolve);
  100. });
  101. // Update delay_ for next time.
  102. this.nextUnfuzzedDelay_ *= this.backoffFactor_;
  103. }
  104. /**
  105. * Gets a copy of the default retry parameters.
  106. *
  107. * @return {shaka.extern.RetryParameters}
  108. */
  109. static defaultRetryParameters() {
  110. // Use a function rather than a constant member so the calling code can
  111. // modify the values without affecting other call results.
  112. return {
  113. maxAttempts: 2,
  114. baseDelay: 1000,
  115. backoffFactor: 2,
  116. fuzzFactor: 0.5,
  117. timeout: 30000,
  118. stallTimeout: 5000,
  119. connectionTimeout: 10000,
  120. };
  121. }
  122. /**
  123. * Fuzz the input value by +/- fuzzFactor. For example, a fuzzFactor of 0.5
  124. * will create a random value that is between 50% and 150% of the input value.
  125. *
  126. * @param {number} value
  127. * @param {number} fuzzFactor
  128. * @return {number} The fuzzed value
  129. * @private
  130. */
  131. static fuzz_(value, fuzzFactor) {
  132. // A random number between -1 and +1.
  133. const negToPosOne = (Math.random() * 2.0) - 1.0;
  134. // A random number between -fuzzFactor and +fuzzFactor.
  135. const negToPosFuzzFactor = negToPosOne * fuzzFactor;
  136. // The original value, fuzzed by +/- fuzzFactor.
  137. return value * (1.0 + negToPosFuzzFactor);
  138. }
  139. /**
  140. * Reset state in autoReset mode.
  141. * @private
  142. */
  143. reset_() {
  144. goog.asserts.assert(this.autoReset_, 'Should only be used for auto-reset!');
  145. this.numAttempts_ = 1;
  146. this.nextUnfuzzedDelay_ = this.baseDelay_;
  147. }
  148. /**
  149. * This method is only public for testing. It allows us to intercept the
  150. * time-delay call.
  151. *
  152. * @param {number} delayInMs
  153. * @param {function()} callback
  154. */
  155. static defer(delayInMs, callback) {
  156. const timer = new shaka.util.Timer(callback);
  157. timer.tickAfter(delayInMs / 1000);
  158. }
  159. };