Browse Source

Add exponential backoff, error dedup, and friendly UX for WebSocket reconnect

Introduce exponential backoff (2s → 60s cap) for WebSocket reconnect
attempts and suppress duplicate close-event error notifications during
reconnect cycles. Improve UX by showing session limit and data size
errors as user-friendly warnings instead of raw error codes.
pull/15219/head
Vladyslav_Prykhodko 6 months ago
parent
commit
0ac3ff676b
  1. 46
      ui-ngx/src/app/core/ws/websocket.service.ts

46
ui-ngx/src/app/core/ws/websocket.service.ts

@ -29,9 +29,11 @@ import {
WebsocketDataMsg WebsocketDataMsg
} from '@shared/models/telemetry/telemetry.models'; } from '@shared/models/telemetry/telemetry.models';
import { ActionNotificationShow } from '@core/notification/notification.actions'; import { ActionNotificationShow } from '@core/notification/notification.actions';
import { NotificationType } from '@core/notification/notification.models';
import Timeout = NodeJS.Timeout; import Timeout = NodeJS.Timeout;
const RECONNECT_INTERVAL = 2000; const RECONNECT_INTERVAL = 2000;
const MAX_RECONNECT_INTERVAL = 60000;
const WS_IDLE_TIMEOUT = 90000; const WS_IDLE_TIMEOUT = 90000;
const MAX_PUBLISH_COMMANDS = 10; const MAX_PUBLISH_COMMANDS = 10;
@ -57,6 +59,15 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
errorName = 'WebSocket Error'; errorName = 'WebSocket Error';
// Exponential backoff: tracks the number of consecutive failed reconnect attempts.
// Reset only after a productive connection (i.e. at least one message received).
// This prevents the open→immediately-closed cycle from resetting the counter.
private reconnectAttempts = 0;
// Suppress duplicate close-event notifications while retrying.
// Set on first close with an error code; cleared after receiving a successful message.
private reconnectErrorShown = false;
protected constructor(protected store: Store<AppState>, protected constructor(protected store: Store<AppState>,
protected authService: AuthService, protected authService: AuthService,
protected ngZone: NgZone, protected ngZone: NgZone,
@ -126,6 +137,8 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
this.subscribersCount = 0; this.subscribersCount = 0;
this.cmdWrapper.clear(); this.cmdWrapper.clear();
if (close) { if (close) {
this.reconnectAttempts = 0;
this.reconnectErrorShown = false;
this.closeSocket(); this.closeSocket();
} }
} }
@ -221,6 +234,10 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
this.processOnMessage(message as WebsocketDataMsg); this.processOnMessage(message as WebsocketDataMsg);
} }
this.checkToClose(); this.checkToClose();
if (this.reconnectAttempts) {
this.reconnectAttempts = 0;
this.reconnectErrorShown = false;
}
} }
private onError(errorEvent) { private onError(errorEvent) {
@ -231,8 +248,11 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
} }
private onClose(closeEvent: CloseEvent) { private onClose(closeEvent: CloseEvent) {
if (closeEvent && closeEvent.code > 1001 && closeEvent.code !== 1006 // Show error notification only once per reconnect cycle to prevent notification spam.
// reconnectErrorShown is cleared only after a productive connection (onMessage).
if (!this.reconnectErrorShown && closeEvent && closeEvent.code > 1001 && closeEvent.code !== 1006
&& closeEvent.code !== 1011 && closeEvent.code !== 1012 && closeEvent.code !== 4500) { && closeEvent.code !== 1011 && closeEvent.code !== 1012 && closeEvent.code !== 4500) {
this.reconnectErrorShown = true;
this.showWsError(closeEvent.code, closeEvent.reason); this.showWsError(closeEvent.code, closeEvent.reason);
} }
this.isOpening = false; this.isOpening = false;
@ -251,18 +271,28 @@ export abstract class WebsocketService<T extends WsSubscriber> implements WsServ
if (this.reconnectTimer) { if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer); clearTimeout(this.reconnectTimer);
} }
this.reconnectTimer = setTimeout(() => this.tryOpenSocket(), RECONNECT_INTERVAL); const delay = Math.min(RECONNECT_INTERVAL * Math.pow(2, this.reconnectAttempts), MAX_RECONNECT_INTERVAL);
this.reconnectAttempts = Math.min(this.reconnectAttempts + 1, 10);
this.reconnectTimer = setTimeout(() => this.tryOpenSocket(), delay);
} }
} }
private showWsError(errorCode: number, errorMsg: string) { private showWsError(errorCode: number, errorMsg: string) {
let message = errorMsg; let message = errorMsg;
if (!message) { let notificationType: NotificationType = 'error';
message += `${this.errorName}: error code - ${errorCode}.`;
if (errorCode === 1008 || (errorMsg && errorMsg.includes('limit reached'))) {
message = 'Too many active sessions. Please close unused browser tabs or sign out from other devices';
notificationType = 'warn';
} else if (errorCode === 1009) {
message = 'Too much data to display. Please refresh the page or narrow your request.';
notificationType = 'warn';
} else if (!message) {
message = `${this.errorName}: error code - ${errorCode}.`;
} }
this.store.dispatch(new ActionNotificationShow(
{ this.store.dispatch(new ActionNotificationShow({
message, type: 'error' message, type: notificationType
})); }));
} }
} }

Loading…
Cancel
Save