UNPKG

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