Skip to content

Commit 79bf7dd

Browse files
[ui] HTTP RPC Engine attempts reconnection until disposal
1 parent 0a412c7 commit 79bf7dd

File tree

1 file changed

+64
-22
lines changed

1 file changed

+64
-22
lines changed

ui/src/trace_processor/http_rpc_engine.ts

Lines changed: 64 additions & 22 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,18 +52,8 @@ 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+
this.websocket ??= this.initWebSocket();
6257

6358
if (this.connected) {
6459
this.websocket.send(data);
@@ -67,27 +62,67 @@ export class HttpRpcEngine extends EngineBase {
6762
}
6863
}
6964

65+
private initWebSocket(): WebSocket {
66+
const wsUrl = `ws://${HttpRpcEngine.getHostAndPort(this.port)}/websocket`;
67+
this.websocket = new WebSocket(wsUrl);
68+
this.websocket.onopen = () => this.onWebsocketConnected();
69+
this.websocket.onmessage = (e) => this.onWebsocketMessage(e);
70+
this.websocket.onclose = (e) => this.onWebsocketClosed(e);
71+
this.websocket.onerror = (e) => this.onWebsocketError(e);
72+
return this.websocket;
73+
}
74+
75+
private onWebsocketError(e: Event): void {
76+
if (this.disposed) return;
77+
const readyState = (e.target as WebSocket)?.readyState;
78+
console.warn(`WebSocket error rs=${readyState}, will retry with backoff`);
79+
// The close event will fire after this, which will trigger the retry logic
80+
}
81+
82+
private scheduleReconnect(): void {
83+
if (this.disposed) return;
84+
85+
console.debug(
86+
`Scheduling WebSocket reconnection in ${this.retryDelayMs}ms`,
87+
);
88+
89+
this.retryTimeoutId = setTimeout(() => {
90+
if (this.disposed) return;
91+
console.debug('Attempting WebSocket reconnection...');
92+
this.initWebSocket();
93+
}, this.retryDelayMs);
94+
95+
// Exponential backoff with cap
96+
this.retryDelayMs = Math.min(
97+
this.retryDelayMs * BACKOFF_MULTIPLIER,
98+
MAX_RETRY_DELAY_MS,
99+
);
100+
}
101+
70102
private onWebsocketConnected() {
103+
// Reset retry delay on successful connection
104+
this.retryDelayMs = INITIAL_RETRY_DELAY_MS;
105+
71106
for (;;) {
72107
const queuedMsg = this.requestQueue.shift();
73108
if (queuedMsg === undefined) break;
74109
assertExists(this.websocket).send(queuedMsg);
75110
}
111+
console.debug('WebSocket (re)connected on port', this.port);
76112
this.connected = true;
77113
}
78114

79115
private onWebsocketClosed(e: CloseEvent) {
80116
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-
}
117+
118+
// Always attempt to reconnect with backoff, regardless of close code
119+
console.debug(
120+
`WebSocket closed (code=${e.code}, reason=${e.reason || 'none'}, wasConnected=${this.connected}), scheduling reconnect`,
121+
);
122+
123+
this.websocket = undefined;
124+
this.connected = false;
125+
this.scheduleReconnect();
91126
}
92127

93128
private onWebsocketMessage(e: MessageEvent) {
@@ -147,6 +182,13 @@ export class HttpRpcEngine extends EngineBase {
147182
[Symbol.dispose]() {
148183
this.disposed = true;
149184
this.connected = false;
185+
186+
// Clear any pending retry timeout
187+
if (this.retryTimeoutId !== undefined) {
188+
clearTimeout(this.retryTimeoutId);
189+
this.retryTimeoutId = undefined;
190+
}
191+
150192
const websocket = this.websocket;
151193
this.websocket = undefined;
152194
websocket?.close();

0 commit comments

Comments
 (0)