Skip to content

Commit 033ef15

Browse files
committed
Improve retry
Registering returns an instance that lets you retry and recover without needing to keep passing the name everywhere. Also refactored the shared process a little to make better use of the retry and downgraded stderr messages to warnings because they aren't critical.
1 parent 3fec7f4 commit 033ef15

File tree

4 files changed

+259
-180
lines changed

4 files changed

+259
-180
lines changed

packages/ide/src/fill/client.ts

+20-10
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class WebsocketConnection implements ReadWriteConnection {
1111
private activeSocket: WebSocket | undefined;
1212
private readonly messageBuffer = <Uint8Array[]>[];
1313
private readonly socketTimeoutDelay = 60 * 1000;
14-
private readonly retryName = "Socket";
14+
private readonly retry = retry.register("Socket", () => this.connect());
1515
private isUp: boolean = false;
1616
private closed: boolean = false;
1717

@@ -26,11 +26,14 @@ class WebsocketConnection implements ReadWriteConnection {
2626
public readonly onMessage = this.messageEmitter.event;
2727

2828
public constructor() {
29-
retry.register(this.retryName, () => this.connect());
30-
retry.block(this.retryName);
31-
retry.run(this.retryName);
29+
this.retry.block();
30+
this.retry.run();
3231
}
3332

33+
/**
34+
* Send data across the socket. If closed, will error. If connecting, will
35+
* queue.
36+
*/
3437
public send(data: Buffer | Uint8Array): void {
3538
if (this.closed) {
3639
throw new Error("web socket is closed");
@@ -42,6 +45,9 @@ class WebsocketConnection implements ReadWriteConnection {
4245
}
4346
}
4447

48+
/**
49+
* Close socket connection.
50+
*/
4551
public close(): void {
4652
this.closed = true;
4753
this.dispose();
@@ -75,8 +81,8 @@ class WebsocketConnection implements ReadWriteConnection {
7581
field("wasClean", event.wasClean),
7682
);
7783
if (!this.closed) {
78-
retry.block(this.retryName);
79-
retry.run(this.retryName);
84+
this.retry.block();
85+
this.retry.run();
8086
}
8187
});
8288

@@ -108,15 +114,19 @@ class WebsocketConnection implements ReadWriteConnection {
108114
}, this.socketTimeoutDelay);
109115

110116
await new Promise((resolve, reject): void => {
111-
const onClose = (): void => {
117+
const doReject = (): void => {
112118
clearTimeout(socketWaitTimeout);
113-
socket.removeEventListener("close", onClose);
119+
socket.removeEventListener("error", doReject);
120+
socket.removeEventListener("close", doReject);
114121
reject();
115122
};
116-
socket.addEventListener("close", onClose);
123+
socket.addEventListener("error", doReject);
124+
socket.addEventListener("close", doReject);
117125

118-
socket.addEventListener("open", async () => {
126+
socket.addEventListener("open", () => {
119127
clearTimeout(socketWaitTimeout);
128+
socket.removeEventListener("error", doReject);
129+
socket.removeEventListener("close", doReject);
120130
resolve();
121131
});
122132
});

packages/ide/src/retry.ts

+105-40
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,64 @@
11
import { logger, field } from "@coder/logger";
22
import { NotificationService, INotificationHandle, INotificationService, Severity } from "./fill/notification";
33

4+
// tslint:disable no-any can have different return values
5+
46
interface IRetryItem {
7+
/**
8+
* How many times this item has been retried.
9+
*/
510
count?: number;
6-
delay?: number; // In seconds.
7-
end?: number; // In ms.
8-
fn(): any | Promise<any>; // tslint:disable-line no-any can have different return values
11+
12+
/**
13+
* In seconds.
14+
*/
15+
delay?: number;
16+
17+
/**
18+
* In milliseconds.
19+
*/
20+
end?: number;
21+
22+
/**
23+
* Function to run when retrying.
24+
*/
25+
fn(): any;
26+
27+
/**
28+
* Timer for running this item.
29+
*/
930
timeout?: number | NodeJS.Timer;
31+
32+
/**
33+
* Whether the item is retrying or waiting to retry.
34+
*/
1035
running?: boolean;
11-
showInNotification: boolean;
36+
}
37+
38+
/**
39+
* An retry-able instance.
40+
*/
41+
export interface RetryInstance {
42+
/**
43+
* Run this retry.
44+
*/
45+
run(error?: Error): void;
46+
47+
/**
48+
* Block on this instance.
49+
*/
50+
block(): void;
51+
}
52+
53+
/**
54+
* A retry-able instance that doesn't use a promise so it must be manually
55+
* ran again on failure and recovered on success.
56+
*/
57+
export interface ManualRetryInstance extends RetryInstance {
58+
/**
59+
* Mark this item as recovered.
60+
*/
61+
recover(): void;
1262
}
1363

1464
/**
@@ -21,7 +71,7 @@ interface IRetryItem {
2171
* to the user explaining what is happening with an option to immediately retry.
2272
*/
2373
export class Retry {
24-
private items = new Map<string, IRetryItem>();
74+
private readonly items = new Map<string, IRetryItem>();
2575

2676
// Times are in seconds.
2777
private readonly retryMinDelay = 1;
@@ -50,13 +100,54 @@ export class Retry {
50100
}
51101

52102
/**
53-
* Block retries when we know they will fail (for example when starting Wush
54-
* back up). If a name is passed, that service will still be allowed to retry
103+
* Register a function to retry that starts/connects to a service.
104+
*
105+
* The service is automatically retried or recovered when the promise resolves
106+
* or rejects. If the service dies after starting, it must be manually
107+
* retried.
108+
*/
109+
public register(name: string, fn: () => Promise<any>): RetryInstance;
110+
/**
111+
* Register a function to retry that starts/connects to a service.
112+
*
113+
* Must manually retry if it fails to start again or dies after restarting and
114+
* manually recover if it succeeds in starting again.
115+
*/
116+
public register(name: string, fn: () => any): ManualRetryInstance;
117+
/**
118+
* Register a function to retry that starts/connects to a service.
119+
*/
120+
public register(name: string, fn: () => any): RetryInstance | ManualRetryInstance {
121+
if (this.items.has(name)) {
122+
throw new Error(`"${name}" is already registered`);
123+
}
124+
this.items.set(name, { fn });
125+
126+
return {
127+
block: (): void => this.block(name),
128+
run: (error?: Error): void => this.run(name, error),
129+
recover: (): void => this.recover(name),
130+
};
131+
}
132+
133+
/**
134+
* Un-register a function to retry.
135+
*/
136+
public unregister(name: string): void {
137+
if (!this.items.has(name)) {
138+
throw new Error(`"${name}" is not registered`);
139+
}
140+
this.items.delete(name);
141+
}
142+
143+
/**
144+
* Block retries when we know they will fail (for example when the socket is
145+
* down ). If a name is passed, that service will still be allowed to retry
55146
* (unless we have already blocked).
56147
*
57148
* Blocking without a name will override a block with a name.
58149
*/
59-
public block(name?: string): void {
150+
private block(name?: string): void {
60151
if (!this.blocked || !name) {
61152
this.blocked = name || true;
62153
this.items.forEach((item) => {
@@ -68,7 +159,7 @@ export class Retry {
68159
/**
69160
* Unblock retries and run any that are pending.
70161
*/
71-
public unblock(): void {
162+
private unblock(): void {
72163
this.blocked = false;
73164
this.items.forEach((item, name) => {
74165
if (item.running) {
@@ -77,35 +168,10 @@ export class Retry {
77168
});
78169
}
79170

80-
/**
81-
* Register a function to retry that starts/connects to a service.
82-
*
83-
* If the function returns a promise, it will automatically be retried,
84-
* recover, & unblock after calling `run` once (otherwise they need to be
85-
* called manually).
86-
*/
87-
// tslint:disable-next-line no-any can have different return values
88-
public register(name: string, fn: () => any | Promise<any>, showInNotification: boolean = true): void {
89-
if (this.items.has(name)) {
90-
throw new Error(`"${name}" is already registered`);
91-
}
92-
this.items.set(name, { fn, showInNotification });
93-
}
94-
95-
/**
96-
* Unregister a function to retry.
97-
*/
98-
public unregister(name: string): void {
99-
if (!this.items.has(name)) {
100-
throw new Error(`"${name}" is not registered`);
101-
}
102-
this.items.delete(name);
103-
}
104-
105171
/**
106172
* Retry a service.
107173
*/
108-
public run(name: string, error?: Error): void {
174+
private run(name: string, error?: Error): void {
109175
if (!this.items.has(name)) {
110176
throw new Error(`"${name}" is not registered`);
111177
}
@@ -149,7 +215,7 @@ export class Retry {
149215
/**
150216
* Reset a service after a successfully recovering.
151217
*/
152-
public recover(name: string): void {
218+
private recover(name: string): void {
153219
if (!this.items.has(name)) {
154220
throw new Error(`"${name}" is not registered`);
155221
}
@@ -191,9 +257,9 @@ export class Retry {
191257
if (this.blocked === name) {
192258
this.unblock();
193259
}
194-
}).catch(() => {
260+
}).catch((error) => {
195261
endItem();
196-
this.run(name);
262+
this.run(name, error);
197263
});
198264
} else {
199265
endItem();
@@ -214,8 +280,7 @@ export class Retry {
214280

215281
const now = Date.now();
216282
const items = Array.from(this.items.entries()).filter(([_, item]) => {
217-
return item.showInNotification
218-
&& typeof item.end !== "undefined"
283+
return typeof item.end !== "undefined"
219284
&& item.end > now
220285
&& item.delay && item.delay >= this.notificationThreshold;
221286
}).sort((a, b) => {

0 commit comments

Comments
 (0)