Improved WS connection failure handling

This commit is contained in:
FinnStutzenstein 2019-09-26 15:08:58 +02:00
parent 7f49ead439
commit 97cda14a04
4 changed files with 55 additions and 60 deletions

View File

@ -1,8 +1,7 @@
import { ApplicationRef, Component } from '@angular/core'; import { Component } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { filter, take } from 'rxjs/operators';
import { ConfigService } from './core/ui-services/config.service'; import { ConfigService } from './core/ui-services/config.service';
import { ConstantsService } from './core/core-services/constants.service'; import { ConstantsService } from './core/core-services/constants.service';
@ -65,7 +64,6 @@ export class AppComponent {
*/ */
public constructor( public constructor(
translate: TranslateService, translate: TranslateService,
appRef: ApplicationRef,
servertimeService: ServertimeService, servertimeService: ServertimeService,
router: Router, router: Router,
operator: OperatorService, operator: OperatorService,
@ -95,15 +93,7 @@ export class AppComponent {
this.overloadFlatMap(); this.overloadFlatMap();
this.overloadModulo(); this.overloadModulo();
// Show the spinner initial servertimeService.startScheduler();
appRef.isStable
.pipe(
// take only the stable state
filter(s => s),
take(1)
)
.subscribe(() => servertimeService.startScheduler());
} }
/** /**

View File

@ -1,6 +1,4 @@
import { ApplicationRef, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { first, take } from 'rxjs/operators';
import { ConstantsService } from './constants.service'; import { ConstantsService } from './constants.service';
import { Deferred } from '../promises/deferred'; import { Deferred } from '../promises/deferred';
@ -21,37 +19,25 @@ export class PingService {
*/ */
private pingInterval: any; private pingInterval: any;
private intervalTime: number; private intervalTime = 30000;
private timeoutTime: number; private timeoutTime = 5000;
private lastLatency: number | null = null; private lastLatency: number | null = null;
public constructor( public constructor(private websocketService: WebsocketService, private constantsService: ConstantsService) {
private websocketService: WebsocketService,
private appRef: ApplicationRef,
private constantsService: ConstantsService
) {
this.setup(); this.setup();
} }
private async setup(): Promise<void> { private async setup(): Promise<void> {
const gotConstants = new Deferred(); const gotConstants = new Deferred();
const isStable = new Deferred();
this.constantsService this.constantsService.get<OpenSlidesSettings>('Settings').subscribe(settings => {
.get<OpenSlidesSettings>('Settings') this.intervalTime = settings.PING_INTERVAL || 30000;
.pipe(take(1)) this.timeoutTime = settings.PING_TIMEOUT || 5000;
.subscribe(settings => { gotConstants.resolve();
this.intervalTime = settings.PING_INTERVAL || 30000;
this.timeoutTime = settings.PING_TIMEOUT || 5000;
gotConstants.resolve();
});
this.appRef.isStable.pipe(first(s => s)).subscribe(() => {
isStable.resolve();
}); });
await gotConstants;
await Promise.all([gotConstants, isStable]);
// Connects the ping-pong mechanism to the opening and closing of the connection. // Connects the ping-pong mechanism to the opening and closing of the connection.
this.websocketService.closeEvent.subscribe(() => this.stopPing()); this.websocketService.closeEvent.subscribe(() => this.stopPing());

View File

@ -32,7 +32,7 @@ export class ServertimeService {
* Starts the scheduler to sync with the server. * Starts the scheduler to sync with the server.
*/ */
public startScheduler(): void { public startScheduler(): void {
this.scheduleNextRefresh(0); this.scheduleNextRefresh(0.1);
} }
/** /**

View File

@ -156,7 +156,8 @@ export class WebsocketService {
/** /**
* The websocket. * The websocket.
*/ */
private websocket: WebSocket; private websocket: WebSocket | null;
private websocketId: string | null;
/** /**
* Subjects for types of websocket messages. A subscriber can get an Observable by {@function getOberservable}. * Subjects for types of websocket messages. A subscriber can get an Observable by {@function getOberservable}.
@ -208,8 +209,14 @@ export class WebsocketService {
* Uses NgZone to let all callbacks run in the angular context. * Uses NgZone to let all callbacks run in the angular context.
*/ */
public async connect(options: ConnectOptions = {}, retry: boolean = false): Promise<void> { public async connect(options: ConnectOptions = {}, retry: boolean = false): Promise<void> {
const websocketId = Math.random()
.toString(36)
.substring(7);
this.websocketId = websocketId;
if (this.websocket) { if (this.websocket) {
await this.close(); this.websocket.close();
this.websocket = null;
} }
if (!retry) { if (!retry) {
@ -244,17 +251,20 @@ export class WebsocketService {
// connection established. If this connect attept was a retry, // connection established. If this connect attept was a retry,
// The error notice will be removed and the reconnectSubject is published. // The error notice will be removed and the reconnectSubject is published.
this.websocket.onopen = (event: Event) => { this.websocket.onopen = (event: Event) => {
if (this.websocketId !== websocketId) {
return;
}
this.zone.run(() => { this.zone.run(() => {
this.retryCounter = 0; this.retryCounter = 0;
if (this.shouldBeClosed) { if (this.shouldBeClosed) {
this.dismissConnectionErrorNotice(); this.offlineService.goOnline();
return; return;
} }
this._connectionOpen = true; this._connectionOpen = true;
if (retry) { if (retry) {
this.dismissConnectionErrorNotice(); this.offlineService.goOnline();
this._retryReconnectEvent.emit(); this._retryReconnectEvent.emit();
} else { } else {
this._noRetryConnectEvent.emit(); this._noRetryConnectEvent.emit();
@ -268,22 +278,31 @@ export class WebsocketService {
}; };
this.websocket.onmessage = (event: MessageEvent) => { this.websocket.onmessage = (event: MessageEvent) => {
if (this.websocketId !== websocketId) {
return;
}
this.zone.run(() => { this.zone.run(() => {
this.handleMessage(event.data); this.handleMessage(event.data);
}); });
}; };
this.websocket.onclose = (event: CloseEvent) => { this.websocket.onclose = (event: CloseEvent) => {
if (this.websocketId !== websocketId) {
return;
}
this.zone.run(() => { this.zone.run(() => {
this.onclose(event.code === 1000); this.onclose();
}); });
}; };
this.websocket.onerror = (event: ErrorEvent) => { this.websocket.onerror = (event: ErrorEvent) => {
if (this.websocketId !== websocketId) {
return;
}
// place for proper error handling and debugging. // place for proper error handling and debugging.
// Required to get more information about errors // Required to get more information about errors
this.zone.run(() => { this.zone.run(() => {
console.warn('Websocket is on Error state. Error: ', event); console.warn('WS error event:', event);
}); });
}; };
} }
@ -353,25 +372,20 @@ export class WebsocketService {
} }
} }
/**
* Simulates an abnormal close.
*/
public simulateAbnormalClose(): void {
if (this.websocket) {
this.websocket.close();
}
this.onclose(false);
}
/** /**
* Closes the connection error notice * Closes the connection error notice
*/ */
private onclose(normalClose: boolean): void { private onclose(): void {
this.websocket = null; if (this.websocket) {
this.websocketId = null; // set to null, so now further events will be
// registered with the line below.
this.websocket.close(); // Cleanup old connection
this.websocket = null;
}
this._connectionOpen = false; this._connectionOpen = false;
// 1000 is a normal close, like the close on logout // 1000 is a normal close, like the close on logout
this._closeEvent.emit(); this._closeEvent.emit();
if (!this.shouldBeClosed && !normalClose) { if (!this.shouldBeClosed) {
// Do not show the message snackbar on the projector // Do not show the message snackbar on the projector
// tests for /projector and /projector/<id> // tests for /projector and /projector/<id>
const onProjector = this.router.url.match(/^\/projector(\/[0-9]+\/?)?$/); const onProjector = this.router.url.match(/^\/projector(\/[0-9]+\/?)?$/);
@ -399,16 +413,12 @@ export class WebsocketService {
} }
} }
private dismissConnectionErrorNotice(): void {
this.offlineService.goOnline();
}
/** /**
* Closes the websocket connection. * Closes the websocket connection.
*/ */
public async close(): Promise<void> { public async close(): Promise<void> {
this.shouldBeClosed = true; this.shouldBeClosed = true;
this.dismissConnectionErrorNotice(); this.offlineService.goOnline();
if (this.websocket) { if (this.websocket) {
this.websocket.close(); this.websocket.close();
this.websocket = null; this.websocket = null;
@ -416,6 +426,15 @@ export class WebsocketService {
} }
} }
/**
* Simulates an abnormal close.
*
* Internally does not set `shouldBeClosed`, so a reconnect is forced.
*/
public simulateAbnormalClose(): void {
this.onclose();
}
/** /**
* closes and reopens the connection. If the connection was closed before, * closes and reopens the connection. If the connection was closed before,
* it will be just opened. * it will be just opened.