backoff-timeout.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. /*
  2. * Copyright 2019 gRPC authors.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. *
  16. */
  17. const INITIAL_BACKOFF_MS = 1000;
  18. const BACKOFF_MULTIPLIER = 1.6;
  19. const MAX_BACKOFF_MS = 120000;
  20. const BACKOFF_JITTER = 0.2;
  21. /**
  22. * Get a number uniformly at random in the range [min, max)
  23. * @param min
  24. * @param max
  25. */
  26. function uniformRandom(min: number, max: number) {
  27. return Math.random() * (max - min) + min;
  28. }
  29. export interface BackoffOptions {
  30. initialDelay?: number;
  31. multiplier?: number;
  32. jitter?: number;
  33. maxDelay?: number;
  34. }
  35. export class BackoffTimeout {
  36. /**
  37. * The delay time at the start, and after each reset.
  38. */
  39. private readonly initialDelay: number = INITIAL_BACKOFF_MS;
  40. /**
  41. * The exponential backoff multiplier.
  42. */
  43. private readonly multiplier: number = BACKOFF_MULTIPLIER;
  44. /**
  45. * The maximum delay time
  46. */
  47. private readonly maxDelay: number = MAX_BACKOFF_MS;
  48. /**
  49. * The maximum fraction by which the delay time can randomly vary after
  50. * applying the multiplier.
  51. */
  52. private readonly jitter: number = BACKOFF_JITTER;
  53. /**
  54. * The delay time for the next time the timer runs.
  55. */
  56. private nextDelay: number;
  57. /**
  58. * The handle of the underlying timer. If running is false, this value refers
  59. * to an object representing a timer that has ended, but it can still be
  60. * interacted with without error.
  61. */
  62. private timerId: NodeJS.Timeout;
  63. /**
  64. * Indicates whether the timer is currently running.
  65. */
  66. private running = false;
  67. /**
  68. * Indicates whether the timer should keep the Node process running if no
  69. * other async operation is doing so.
  70. */
  71. private hasRef = true;
  72. /**
  73. * The time that the currently running timer was started. Only valid if
  74. * running is true.
  75. */
  76. private startTime: Date = new Date();
  77. /**
  78. * The approximate time that the currently running timer will end. Only valid
  79. * if running is true.
  80. */
  81. private endTime: Date = new Date();
  82. constructor(private callback: () => void, options?: BackoffOptions) {
  83. if (options) {
  84. if (options.initialDelay) {
  85. this.initialDelay = options.initialDelay;
  86. }
  87. if (options.multiplier) {
  88. this.multiplier = options.multiplier;
  89. }
  90. if (options.jitter) {
  91. this.jitter = options.jitter;
  92. }
  93. if (options.maxDelay) {
  94. this.maxDelay = options.maxDelay;
  95. }
  96. }
  97. this.nextDelay = this.initialDelay;
  98. this.timerId = setTimeout(() => {}, 0);
  99. clearTimeout(this.timerId);
  100. }
  101. private runTimer(delay: number) {
  102. this.endTime = this.startTime;
  103. this.endTime.setMilliseconds(
  104. this.endTime.getMilliseconds() + this.nextDelay
  105. );
  106. clearTimeout(this.timerId);
  107. this.timerId = setTimeout(() => {
  108. this.callback();
  109. this.running = false;
  110. }, delay);
  111. if (!this.hasRef) {
  112. this.timerId.unref?.();
  113. }
  114. }
  115. /**
  116. * Call the callback after the current amount of delay time
  117. */
  118. runOnce() {
  119. this.running = true;
  120. this.startTime = new Date();
  121. this.runTimer(this.nextDelay);
  122. const nextBackoff = Math.min(
  123. this.nextDelay * this.multiplier,
  124. this.maxDelay
  125. );
  126. const jitterMagnitude = nextBackoff * this.jitter;
  127. this.nextDelay =
  128. nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude);
  129. }
  130. /**
  131. * Stop the timer. The callback will not be called until `runOnce` is called
  132. * again.
  133. */
  134. stop() {
  135. clearTimeout(this.timerId);
  136. this.running = false;
  137. }
  138. /**
  139. * Reset the delay time to its initial value. If the timer is still running,
  140. * retroactively apply that reset to the current timer.
  141. */
  142. reset() {
  143. this.nextDelay = this.initialDelay;
  144. if (this.running) {
  145. const now = new Date();
  146. const newEndTime = this.startTime;
  147. newEndTime.setMilliseconds(newEndTime.getMilliseconds() + this.nextDelay);
  148. clearTimeout(this.timerId);
  149. if (now < newEndTime) {
  150. this.runTimer(newEndTime.getTime() - now.getTime());
  151. } else {
  152. this.running = false;
  153. }
  154. }
  155. }
  156. /**
  157. * Check whether the timer is currently running.
  158. */
  159. isRunning() {
  160. return this.running;
  161. }
  162. /**
  163. * Set that while the timer is running, it should keep the Node process
  164. * running.
  165. */
  166. ref() {
  167. this.hasRef = true;
  168. this.timerId.ref?.();
  169. }
  170. /**
  171. * Set that while the timer is running, it should not keep the Node process
  172. * running.
  173. */
  174. unref() {
  175. this.hasRef = false;
  176. this.timerId.unref?.();
  177. }
  178. /**
  179. * Get the approximate timestamp of when the timer will fire. Only valid if
  180. * this.isRunning() is true.
  181. */
  182. getEndTime() {
  183. return this.endTime;
  184. }
  185. }