UNPKG

4.72 kBPlain TextView Raw
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
18const INITIAL_BACKOFF_MS = 1000;
19const BACKOFF_MULTIPLIER = 1.6;
20const MAX_BACKOFF_MS = 120000;
21const BACKOFF_JITTER = 0.2;
22
23/**
24 * Get a number uniformly at random in the range [min, max)
25 * @param min
26 * @param max
27 */
28function uniformRandom(min: number, max: number) {
29 return Math.random() * (max - min) + min;
30}
31
32export interface BackoffOptions {
33 initialDelay?: number;
34 multiplier?: number;
35 jitter?: number;
36 maxDelay?: number;
37}
38
39export class BackoffTimeout {
40 /**
41 * The delay time at the start, and after each reset.
42 */
43 private readonly initialDelay: number = INITIAL_BACKOFF_MS;
44 /**
45 * The exponential backoff multiplier.
46 */
47 private readonly multiplier: number = BACKOFF_MULTIPLIER;
48 /**
49 * The maximum delay time
50 */
51 private readonly maxDelay: number = MAX_BACKOFF_MS;
52 /**
53 * The maximum fraction by which the delay time can randomly vary after
54 * applying the multiplier.
55 */
56 private readonly jitter: number = BACKOFF_JITTER;
57 /**
58 * The delay time for the next time the timer runs.
59 */
60 private nextDelay: number;
61 /**
62 * The handle of the underlying timer. If running is false, this value refers
63 * to an object representing a timer that has ended, but it can still be
64 * interacted with without error.
65 */
66 private timerId: NodeJS.Timer;
67 /**
68 * Indicates whether the timer is currently running.
69 */
70 private running = false;
71 /**
72 * Indicates whether the timer should keep the Node process running if no
73 * other async operation is doing so.
74 */
75 private hasRef = true;
76 /**
77 * The time that the currently running timer was started. Only valid if
78 * running is true.
79 */
80 private startTime: Date = new Date();
81
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
102 private runTimer(delay: number) {
103 clearTimeout(this.timerId);
104 this.timerId = setTimeout(() => {
105 this.callback();
106 this.running = false;
107 }, delay);
108 if (!this.hasRef) {
109 this.timerId.unref?.();
110 }
111 }
112
113 /**
114 * Call the callback after the current amount of delay time
115 */
116 runOnce() {
117 this.running = true;
118 this.startTime = new Date();
119 this.runTimer(this.nextDelay);
120 const nextBackoff = Math.min(
121 this.nextDelay * this.multiplier,
122 this.maxDelay
123 );
124 const jitterMagnitude = nextBackoff * this.jitter;
125 this.nextDelay =
126 nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude);
127 }
128
129 /**
130 * Stop the timer. The callback will not be called until `runOnce` is called
131 * again.
132 */
133 stop() {
134 clearTimeout(this.timerId);
135 this.running = false;
136 }
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 /**
158 * Check whether the timer is currently running.
159 */
160 isRunning() {
161 return this.running;
162 }
163
164 /**
165 * Set that while the timer is running, it should keep the Node process
166 * running.
167 */
168 ref() {
169 this.hasRef = true;
170 this.timerId.ref?.();
171 }
172
173 /**
174 * Set that while the timer is running, it should not keep the Node process
175 * running.
176 */
177 unref() {
178 this.hasRef = false;
179 this.timerId.unref?.();
180 }
181}