Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix websocket 1006 timeouts #2172

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 44 additions & 36 deletions client/nginx.vh.default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,57 @@ gzip on;
gzip_vary on;
gzip_proxied any;
gzip_types
text/css
text/javascript
text/xml
text/plain
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/atom+xml
font/truetype
font/opentype
image/svg+xml;
text/css
text/javascript
text/xml
text/plain
application/javascript
application/x-javascript
application/json
application/xml
application/rss+xml
application/atom+xml
font/truetype
font/opentype
image/svg+xml;

server {
listen 8080;
# server_name localhost;
listen 8080;

#charset koi8-r;
#access_log /var/log/nginx/host.access.log main;
root /usr/share/nginx/html;
index index.html;

root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}

location / {
try_files $uri $uri/ /index.html;
}
location ~* .(?:css|js)$ {
expires 7d;
add_header Cache-Control "public";
}

location ~* .(?:css|js)$ {
expires 7d;
add_header Cache-Control "public";
}
location ~* .(?:jpg|png|svg)$ {
expires 7d;
add_header Cache-Control "public";
}

location ~* .(?:jpg|png|svg)$ {
expires 7d;
add_header Cache-Control "public";
}
location /config.json {
add_header Cache-Control "no-cache, max-age=-1";
}

location /config.json {
add_header Cache-Control "no-cache, max-age=-1";
}
location /manifest.json {
add_header Cache-Control "no-cache, max-age=-1";
}

location /manifest.json {
add_header Cache-Control "no-cache, max-age=-1";
}
# WebSockets configuration
location /ui-server/ws {
# ? REF: http://nginx.org/en/docs/http/websocket.html
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# ? REF: https://stackoverflow.com/questions/10550558/nginx-tcp-websockets-timeout-keepalive-config
proxy_connect_timeout 1d;
proxy_send_timeout 1d;
proxy_read_timeout 1d;
}
}
20 changes: 19 additions & 1 deletion client/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ const messageHandlers: Record<string, Record<string, Array<MessageData>>> = {
handler: handleUserUiVersion
}
],
"ping": [
{
required: ["message"],
optional: ["source"],
handler: handleServerPing
}
],
"ack": [
{
required: null,
Expand All @@ -74,6 +81,17 @@ const messageHandlers: Record<string, Record<string, Array<MessageData>>> = {
};


function handleServerPing(data: Record<string, unknown>, webSocket: WebSocket, model: any): boolean {
let pingMessage: WsMessage;
if (data.message === "ping" && data.source === "server") {
pingMessage = new WsMessage({ message: "ack" }, "ping");
webSocket.send(pingMessage.toString());
return true;
}
return false;
}


// *** WebSocket startup and configuration ***

/**
Expand All @@ -88,7 +106,7 @@ function setupWebSocket(webSocketUrl: string, fullModel: any) {

function pingWebSocketServer(targetWebSocket: WebSocket) {
if (model.get("open") && targetWebSocket.readyState === targetWebSocket.OPEN) {
const pingMessage = new WsMessage({}, "ping");
const pingMessage = new WsMessage({ message: "ping", source: "client" }, "ping");
targetWebSocket.send(pingMessage.toString());
model.setObject({ lastPing: new Date(pingMessage.timestamp) });
setTimeout(() => pingWebSocketServer(targetWebSocket), timeoutIntervalMs);
Expand Down
2 changes: 2 additions & 0 deletions server/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ const DATA = {

const WEBSOCKET = {
enabled: convertType(process.env.WEBSOCKET_ENABLED) || true,
pingIntervalSec: 30, // ? in seconds
pingIntervalUncertaintySec: 5, // ? in seconds
shortIntervalSec: 5, // ? in seconds
longIntervalSec: 180, // ? in seconds
delayStartSec: 3, // ? in seconds
Expand Down
49 changes: 48 additions & 1 deletion server/src/websocket/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,47 @@ async function channelShortLoop(sessionId: string, authenticator: Authenticator,
}


/**
* This function pings the channel to prevent timing out the connection early.
* @param sessionId - user session ID
* @param authenticator - auth component
*/
async function channelPingLoop(sessionId: string, authenticator: Authenticator) {
const infoPrefix = `${sessionId} - ping loop:`;

// checking user
const channel = channels.get(sessionId);
if (!channel) {
logger.info(`${infoPrefix} no channels detected, ending loop.`);
return false;
}

// checking authentication
const fixedDelta = config.websocket.pingIntervalSec as number;
const variableDelta = Math.floor(Math.random() + config.websocket.pingIntervalUncertaintySec as number + 1);
const timeoutLength = (fixedDelta + variableDelta) * 1000;
if (!authenticator.ready) {
logger.info(`${infoPrefix} Authenticator not ready yet, skipping to the next loop`);
setTimeout(() => channelPingLoop(sessionId, authenticator), timeoutLength);
return;
}

// get the auth headers
const authHeaders = await getAuthHeaders(authenticator, sessionId, infoPrefix);
if (authHeaders instanceof WsMessage && authHeaders.data.expired) {
// ? here authHeaders is an error message
channel.sockets.forEach(socket => socket.send(authHeaders.toString()));
channels.delete(sessionId);
return false;
}

// Ping to keep the socket alive, then reschedule loop
const ping = new WsMessage({ message: "ping", source: "server" }, "user", "ping");
channel.sockets.forEach(socket => socket.send(ping.toString()));
setTimeout(() => channelPingLoop(sessionId, authenticator), timeoutLength);
}


// *** WebSocket startup and configuration ***
// We might want to increase the `proxy_read_timeout` in nginx, otherwise connection terminates after 60 seconds
// REF: http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
Expand Down Expand Up @@ -220,7 +261,13 @@ function configureWebsocket(server: ws.Server, authenticator: Authenticator, sto
setTimeout(() => {
channelShortLoop(sessionId, authenticator, storage);
// add a tiny buffer, in case authentication fails and channel is cleaned up -- no need to overlap
setTimeout(() => { channelLongLoop(sessionId, authenticator, storage); }, 1000);
setTimeout(() => {
channelLongLoop(sessionId, authenticator, storage);
// start pinging after all these steps
setTimeout(() => { // eslint-disable-line max-nested-callbacks
channelPingLoop(sessionId, authenticator);
}, config.websocket.pingIntervalUncertaintySec * 1000);
}, 1000);
}, config.websocket.delayStartSec * 1000);
}

Expand Down