Source: lib/net/backoff.js

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