Skip to content

Commit 061c738

Browse files
[ui] HTTP RPC Engine attempts reconnection until disposal (#168)
1 parent 0a412c7 commit 061c738

File tree

1 file changed

+98
-26
lines changed

1 file changed

+98
-26
lines changed

ui/src/trace_processor/http_rpc_engine.ts

Lines changed: 98 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import {assertExists, reportError} from '../base/logging';
1818
import {EngineBase} from '../trace_processor/engine';
1919

2020
const RPC_CONNECT_TIMEOUT_MS = 2000;
21+
const INITIAL_RETRY_DELAY_MS = 100;
22+
const MAX_RETRY_DELAY_MS = 30000;
23+
const BACKOFF_MULTIPLIER = 2;
2124

2225
export interface HttpRpcState {
2326
connected: boolean;
@@ -34,6 +37,8 @@ export class HttpRpcEngine extends EngineBase {
3437
private disposed = false;
3538
private queue: Blob[] = [];
3639
private isProcessingQueue = false;
40+
private retryDelayMs = INITIAL_RETRY_DELAY_MS;
41+
private retryTimeoutId?: ReturnType<typeof setTimeout>;
3742

3843
// Can be changed by frontend/index.ts when passing ?rpc_port=1234 .
3944
static defaultRpcPort = '9001';
@@ -47,47 +52,109 @@ export class HttpRpcEngine extends EngineBase {
4752
}
4853

4954
rpcSendRequestBytes(data: Uint8Array): void {
50-
if (this.websocket === undefined) {
51-
if (this.disposed) return;
52-
const wsUrl = `ws://${HttpRpcEngine.getHostAndPort(this.port)}/websocket`;
53-
this.websocket = new WebSocket(wsUrl);
54-
this.websocket.onopen = () => this.onWebsocketConnected();
55-
this.websocket.onmessage = (e) => this.onWebsocketMessage(e);
56-
this.websocket.onclose = (e) => this.onWebsocketClosed(e);
57-
this.websocket.onerror = (e) =>
58-
super.fail(
59-
`WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`,
60-
);
61-
}
55+
if (this.disposed) return;
56+
const websocket = this.getOrCreateWebSocket();
6257

6358
if (this.connected) {
64-
this.websocket.send(data);
59+
websocket.send(data);
6560
} else {
6661
this.requestQueue.push(data); // onWebsocketConnected() will flush this.
6762
}
6863
}
6964

65+
/**
66+
* Returns the existing WebSocket if one exists and is not closed,
67+
* otherwise creates a new one (closing any stale socket first).
68+
*/
69+
private getOrCreateWebSocket(): WebSocket {
70+
// If we have an active websocket that's not closed/closing, reuse it
71+
if (
72+
this.websocket !== undefined &&
73+
this.websocket.readyState !== WebSocket.CLOSED &&
74+
this.websocket.readyState !== WebSocket.CLOSING
75+
) {
76+
return this.websocket;
77+
}
78+
79+
// Close any stale websocket before creating a new one
80+
this.closeWebSocket();
81+
82+
const wsUrl = `ws://${HttpRpcEngine.getHostAndPort(this.port)}/websocket`;
83+
this.websocket = new WebSocket(wsUrl);
84+
this.websocket.onopen = () => this.onWebsocketConnected();
85+
this.websocket.onmessage = (e) => this.onWebsocketMessage(e);
86+
this.websocket.onclose = (e) => this.onWebsocketClosed(e);
87+
this.websocket.onerror = (e) => this.onWebsocketError(e);
88+
return this.websocket;
89+
}
90+
91+
/**
92+
* Closes the current websocket if one exists, clearing event handlers
93+
* to prevent spurious callbacks.
94+
*/
95+
private closeWebSocket(): void {
96+
if (this.websocket === undefined) return;
97+
98+
// Clear handlers to prevent callbacks from a closing socket
99+
this.websocket.onopen = null;
100+
this.websocket.onmessage = null;
101+
this.websocket.onclose = null;
102+
this.websocket.onerror = null;
103+
this.websocket.close();
104+
this.websocket = undefined;
105+
}
106+
107+
private onWebsocketError(e: Event): void {
108+
if (this.disposed) return;
109+
const readyState = (e.target as WebSocket)?.readyState;
110+
console.warn(`WebSocket error rs=${readyState}, will retry with backoff`);
111+
// The close event will fire after this, which will trigger the retry logic
112+
}
113+
114+
private scheduleReconnect(): void {
115+
if (this.disposed) return;
116+
117+
console.debug(
118+
`Scheduling WebSocket reconnection in ${this.retryDelayMs}ms`,
119+
);
120+
121+
this.retryTimeoutId = setTimeout(() => {
122+
if (this.disposed) return;
123+
console.debug('Attempting WebSocket reconnection...');
124+
this.getOrCreateWebSocket();
125+
}, this.retryDelayMs);
126+
127+
// Exponential backoff with cap
128+
this.retryDelayMs = Math.min(
129+
this.retryDelayMs * BACKOFF_MULTIPLIER,
130+
MAX_RETRY_DELAY_MS,
131+
);
132+
}
133+
70134
private onWebsocketConnected() {
135+
// Reset retry delay on successful connection
136+
this.retryDelayMs = INITIAL_RETRY_DELAY_MS;
137+
71138
for (;;) {
72139
const queuedMsg = this.requestQueue.shift();
73140
if (queuedMsg === undefined) break;
74141
assertExists(this.websocket).send(queuedMsg);
75142
}
143+
console.debug('WebSocket (re)connected on port', this.port);
76144
this.connected = true;
77145
}
78146

79147
private onWebsocketClosed(e: CloseEvent) {
80148
if (this.disposed) return;
81-
if (e.code === 1006 && this.connected) {
82-
// On macbooks the act of closing the lid / suspending often causes socket
83-
// disconnections. Try to gracefully re-connect.
84-
console.log('Websocket closed, reconnecting');
85-
this.websocket = undefined;
86-
this.connected = false;
87-
this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection.
88-
} else {
89-
super.fail(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`);
90-
}
149+
150+
// Always attempt to reconnect with backoff, regardless of close code
151+
console.debug(
152+
`WebSocket closed (code=${e.code}, reason=${e.reason || 'none'}, wasConnected=${this.connected}), scheduling reconnect`,
153+
);
154+
155+
this.websocket = undefined;
156+
this.connected = false;
157+
this.scheduleReconnect();
91158
}
92159

93160
private onWebsocketMessage(e: MessageEvent) {
@@ -147,8 +214,13 @@ export class HttpRpcEngine extends EngineBase {
147214
[Symbol.dispose]() {
148215
this.disposed = true;
149216
this.connected = false;
150-
const websocket = this.websocket;
151-
this.websocket = undefined;
152-
websocket?.close();
217+
218+
// Clear any pending retry timeout
219+
if (this.retryTimeoutId !== undefined) {
220+
clearTimeout(this.retryTimeoutId);
221+
this.retryTimeoutId = undefined;
222+
}
223+
224+
this.closeWebSocket();
153225
}
154226
}

0 commit comments

Comments
 (0)