Skip to content
This repository has been archived by the owner on Mar 13, 2024. It is now read-only.

Commit

Permalink
MM-20664 Add client side ping pong (#6052)
Browse files Browse the repository at this point in the history
* MM-20664 Add client side ping pong

  * Make ping event after every 10secs
  * clear timer to not create ping events on new message
  * close conn if server does not respond with a pong in due time

* lint fix

* Update client/websocket_client.test.jsx

Co-authored-by: Guillermo Vayá <[email protected]>

* Clear ping and pong  on close

* Fix constant

* Fix clear of pingpong

* Change to use conn.onClose when closing connection

Co-authored-by: Guillermo Vayá <[email protected]>
Co-authored-by: Mattermod <[email protected]>
  • Loading branch information
3 people authored Sep 11, 2020
1 parent d556bcd commit 58bb453
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 1 deletion.
29 changes: 29 additions & 0 deletions client/websocket_client.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
const MAX_WEBSOCKET_FAILS = 7;
const MIN_WEBSOCKET_RETRY_TIME = 3000; // 3 sec
const MAX_WEBSOCKET_RETRY_TIME = 300000; // 5 mins
const PING_TIME = 10000;
const PONG_WAIT_TIME = 2000;

export default class WebSocketClient {
constructor() {
Expand All @@ -19,6 +21,7 @@ export default class WebSocketClient {
this.missedEventCallback = null;
this.errorCallback = null;
this.closeCallback = null;
this.pingTimer = null;
}

initialize(connectionUrl = this.connectionUrl, token) {
Expand Down Expand Up @@ -54,6 +57,8 @@ export default class WebSocketClient {
this.firstConnectCallback();
}

this.clearPingPong();
this.createPingEvent();
this.connectFailCount = 0;
};

Expand All @@ -65,6 +70,7 @@ export default class WebSocketClient {
console.log('websocket closed'); //eslint-disable-line no-console
}

this.clearPingPong();
this.connectFailCount++;

if (this.closeCallback) {
Expand Down Expand Up @@ -98,6 +104,7 @@ export default class WebSocketClient {
if (this.errorCallback) {
this.errorCallback(evt);
}
this.clearPingPong();
};

this.conn.onmessage = (evt) => {
Expand All @@ -119,6 +126,9 @@ export default class WebSocketClient {
this.eventSequence = msg.seq + 1;
this.eventCallback(msg);
}

this.clearPingPong();
this.createPingEvent();
};
}

Expand Down Expand Up @@ -150,6 +160,7 @@ export default class WebSocketClient {
this.connectFailCount = 0;
this.sequence = 1;
if (this.conn && this.conn.readyState === WebSocket.OPEN) {
this.clearPingPong();
this.conn.onclose = () => {}; //eslint-disable-line no-empty-function
this.conn.close();
this.conn = null;
Expand Down Expand Up @@ -201,4 +212,22 @@ export default class WebSocketClient {
data.user_ids = userIds;
this.sendMessage('get_statuses_by_ids', data, callback);
}

createPingEvent() {
this.pingTimer = setInterval(() => {
this.sendMessage('ping');
this.waitForPong();
}, PING_TIME);
}

waitForPong() {
this.pongTimer = setTimeout(() => {
this.conn.onclose();
}, PONG_WAIT_TIME);
}

clearPingPong() {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
}
}
69 changes: 69 additions & 0 deletions client/websocket_client.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import WebSocketClient from './websocket_client.jsx';
const FAKE_URL = 'ws:https://localhostt:9001';

describe('WebSocketClient', () => {
test('should call for createPingEvent on conn open and clear existing ping pong timers', () => {
const clientSocket = new WebSocketClient();
clientSocket.createPingEvent = jest.fn();
clientSocket.initialize(FAKE_URL);
clientSocket.conn.onopen();
expect(clientSocket.createPingEvent).toHaveBeenCalled();
expect(clearTimeout).toHaveBeenCalledWith(clientSocket.pongTimer);
expect(clearInterval).toHaveBeenCalledWith(clientSocket.pingTimer);
});

test('should clear ping pong timers on close of connection', () => {
const clientSocket = new WebSocketClient();
clientSocket.initialize(FAKE_URL);
clientSocket.conn.onclose();
expect(clearTimeout).toHaveBeenCalledWith(clientSocket.pongTimer);
expect(clearInterval).toHaveBeenCalledWith(clientSocket.pingTimer);
});

test('should clear ping pong timers on error of connection', () => {
const clientSocket = new WebSocketClient();
clientSocket.initialize(FAKE_URL);
clientSocket.conn.onerror();
expect(clearTimeout).toHaveBeenCalledWith(clientSocket.pongTimer);
expect(clearInterval).toHaveBeenCalledWith(clientSocket.pingTimer);
});

test('should call for createPingEvent on onmessage and clear existing ping pong timers', () => {
const clientSocket = new WebSocketClient();
clientSocket.createPingEvent = jest.fn();
clientSocket.initialize(FAKE_URL);
clientSocket.conn.onmessage({data: '{}'});
expect(clientSocket.createPingEvent).toHaveBeenCalled();
expect(clearTimeout).toHaveBeenCalledWith(clientSocket.pongTimer);
expect(clearInterval).toHaveBeenCalledWith(clientSocket.pingTimer);
});

test('should call sendMessage on call of createPingEvent', () => {
const clientSocket = new WebSocketClient();
clientSocket.sendMessage = jest.fn();
clientSocket.createPingEvent();
expect(clientSocket.sendMessage).not.toHaveBeenCalled();
jest.runTimersToTime(10000);
expect(clientSocket.sendMessage).toHaveBeenCalledWith('ping');
clientSocket.clearPingPong();
});

test('should call conn.onclose if waitForPong is called and timer is not cleared', () => {
const clientSocket = new WebSocketClient();
clientSocket.conn = {
onclose: jest.fn(),
};

clientSocket.waitForPong();
expect(clientSocket.conn.onclose).not.toHaveBeenCalled();
clientSocket.clearPingPong();
jest.runTimersToTime(2000);
expect(clientSocket.conn.onclose).not.toHaveBeenCalled();
clientSocket.waitForPong();
jest.runTimersToTime(2000);
expect(clientSocket.conn.onclose).toHaveBeenCalled();
});
});
2 changes: 1 addition & 1 deletion tests/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ beforeEach(() => {
afterEach(() => {
if (logs.length > 0 || warns.length > 0 || errors.length > 0) {
const message = 'Unexpected console logs' + logs + warns + errors;
if (message.includes('componentWillReceiveProps')) {
if (message.includes('componentWillReceiveProps') || message.includes('websocket')) {
return;
}
throw new Error(message);
Expand Down

0 comments on commit 58bb453

Please sign in to comment.