UNPKG

5.17 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.Timeout;
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 * The approximate time that the currently running timer will end. Only valid
83 * if running is true.
84 */
85 private endTime: Date = new Date();
86
87 constructor(private callback: () => void, options?: BackoffOptions) {
88 if (options) {
89 if (options.initialDelay) {
90 this.initialDelay = options.initialDelay;
91 }
92 if (options.multiplier) {
93 this.multiplier = options.multiplier;
94 }
95 if (options.jitter) {
96 this.jitter = options.jitter;
97 }
98 if (options.maxDelay) {
99 this.maxDelay = options.maxDelay;
100 }
101 }
102 this.nextDelay = this.initialDelay;
103 this.timerId = setTimeout(() => {}, 0);
104 clearTimeout(this.timerId);
105 }
106
107 private runTimer(delay: number) {
108 this.endTime = this.startTime;
109 this.endTime.setMilliseconds(
110 this.endTime.getMilliseconds() + this.nextDelay
111 );
112 clearTimeout(this.timerId);
113 this.timerId = setTimeout(() => {
114 this.callback();
115 this.running = false;
116 }, delay);
117 if (!this.hasRef) {
118 this.timerId.unref?.();
119 }
120 }
121
122 /**
123 * Call the callback after the current amount of delay time
124 */
125 runOnce() {
126 this.running = true;
127 this.startTime = new Date();
128 this.runTimer(this.nextDelay);
129 const nextBackoff = Math.min(
130 this.nextDelay * this.multiplier,
131 this.maxDelay
132 );
133 const jitterMagnitude = nextBackoff * this.jitter;
134 this.nextDelay =
135 nextBackoff + uniformRandom(-jitterMagnitude, jitterMagnitude);
136 }
137
138 /**
139 * Stop the timer. The callback will not be called until `runOnce` is called
140 * again.
141 */
142 stop() {
143 clearTimeout(this.timerId);
144 this.running = false;
145 }
146
147 /**
148 * Reset the delay time to its initial value. If the timer is still running,
149 * retroactively apply that reset to the current timer.
150 */
151 reset() {
152 this.nextDelay = this.initialDelay;
153 if (this.running) {
154 const now = new Date();
155 const newEndTime = this.startTime;
156 newEndTime.setMilliseconds(newEndTime.getMilliseconds() + this.nextDelay);
157 clearTimeout(this.timerId);
158 if (now < newEndTime) {
159 this.runTimer(newEndTime.getTime() - now.getTime());
160 } else {
161 this.running = false;
162 }
163 }
164 }
165
166 /**
167 * Check whether the timer is currently running.
168 */
169 isRunning() {
170 return this.running;
171 }
172
173 /**
174 * Set that while the timer is running, it should keep the Node process
175 * running.
176 */
177 ref() {
178 this.hasRef = true;
179 this.timerId.ref?.();
180 }
181
182 /**
183 * Set that while the timer is running, it should not keep the Node process
184 * running.
185 */
186 unref() {
187 this.hasRef = false;
188 this.timerId.unref?.();
189 }
190
191 /**
192 * Get the approximate timestamp of when the timer will fire. Only valid if
193 * this.isRunning() is true.
194 */
195 getEndTime() {
196 return this.endTime;
197 }
198}