From 5ab44b968cd71a28ae9c958e1aa5bb69c0369e3a Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 15:01:26 +0200 Subject: [PATCH 1/6] feat: support AWS WebSocket hard timeout --- src/events/websocket/WebSocketClients.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/events/websocket/WebSocketClients.js b/src/events/websocket/WebSocketClients.js index 9561cda66..7e94749e0 100644 --- a/src/events/websocket/WebSocketClients.js +++ b/src/events/websocket/WebSocketClients.js @@ -127,6 +127,11 @@ export default class WebSocketClients { this._processEvent(webSocketClient, connectionId, '$connect', connectEvent) + const hardTimeout = setTimeout(() => { + debugLog(`timeout:hard:${connectionId}`) + webSocketClient.close() + }, 2 * 3600 * 1000) + webSocketClient.on('close', () => { debugLog(`disconnect:${connectionId}`) @@ -136,6 +141,8 @@ export default class WebSocketClients { connectionId, ).create() + clearTimeout(hardTimeout) + this._processEvent( webSocketClient, connectionId, From a7a91f4d84a2daef7d946e627fa3f3843edf81a9 Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 15:04:26 +0200 Subject: [PATCH 2/6] feat: add webSocketHardTimeout option --- README.md | 1 + src/config/commandOptions.js | 4 ++++ src/config/defaultOptions.js | 1 + src/events/websocket/WebSocketClients.js | 2 +- 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c75bb72a..989c8b5f2 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ All CLI options are optional: --useChildProcesses Run handlers in a child process --useWorkerThreads Uses worker threads for handlers. Requires node.js v11.7.0 or higher --websocketPort WebSocket port to listen on. Default: 3001 +--webSocketHardTimeout Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours) --useDocker Run handlers in a docker container. ``` diff --git a/src/config/commandOptions.js b/src/config/commandOptions.js index baacd44a2..7035e9211 100644 --- a/src/config/commandOptions.js +++ b/src/config/commandOptions.js @@ -78,6 +78,10 @@ export default { websocketPort: { usage: 'Websocket port to listen on. Default: 3001', }, + webSocketHardTimeout: { + usage: + 'Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours)', + }, useDocker: { usage: 'Uses docker for node/python/ruby', }, diff --git a/src/config/defaultOptions.js b/src/config/defaultOptions.js index 1da5b7404..55c24b2ef 100644 --- a/src/config/defaultOptions.js +++ b/src/config/defaultOptions.js @@ -22,6 +22,7 @@ export default { useChildProcesses: false, useWorkerThreads: false, websocketPort: 3001, + webSocketHardTimeout: 7200, useDocker: false, functionCleanupIdleTimeSeconds: 60, } diff --git a/src/events/websocket/WebSocketClients.js b/src/events/websocket/WebSocketClients.js index 7e94749e0..e8bef068a 100644 --- a/src/events/websocket/WebSocketClients.js +++ b/src/events/websocket/WebSocketClients.js @@ -130,7 +130,7 @@ export default class WebSocketClients { const hardTimeout = setTimeout(() => { debugLog(`timeout:hard:${connectionId}`) webSocketClient.close() - }, 2 * 3600 * 1000) + }, this.#options.webSocketHardTimeout * 1000) webSocketClient.on('close', () => { debugLog(`disconnect:${connectionId}`) From d186ec05325316dd4695b8bffe16a771a2e7e7bc Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 16:50:11 +0200 Subject: [PATCH 3/6] feat: support AWS WebSocket idle timeout --- src/events/websocket/WebSocketClients.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/events/websocket/WebSocketClients.js b/src/events/websocket/WebSocketClients.js index e8bef068a..f7d0b3aab 100644 --- a/src/events/websocket/WebSocketClients.js +++ b/src/events/websocket/WebSocketClients.js @@ -20,6 +20,7 @@ export default class WebSocketClients { #options = null #webSocketRoutes = new Map() #websocketsApiRouteSelectionExpression = null + #idleTimeouts = new WeakMap() constructor(serverless, options, lambda) { this.#lambda = lambda @@ -32,6 +33,7 @@ export default class WebSocketClients { _addWebSocketClient(client, connectionId) { this.#clients.set(client, connectionId) this.#clients.set(connectionId, client) + this._onWebSocketUsed(connectionId) } _removeWebSocketClient(client) { @@ -39,6 +41,7 @@ export default class WebSocketClients { this.#clients.delete(client) this.#clients.delete(connectionId) + this.#idleTimeouts.delete(connectionId, client) return connectionId } @@ -47,6 +50,22 @@ export default class WebSocketClients { return this.#clients.get(connectionId) } + _onWebSocketUsed(connectionId) { + const client = this._getWebSocketClient(connectionId) + this._clearIdleTimeout(client) + + const timeoutId = setTimeout(() => { + debugLog(`timeout:idle:${connectionId}`) + client.close(1001, 'Going away') + }, 10 * 1000) + this.#idleTimeouts.set(client, timeoutId) + } + + _clearIdleTimeout(client) { + const timeoutId = this.#idleTimeouts.get(client) + clearTimeout(timeoutId) + } + async _processEvent(websocketClient, connectionId, route, event) { let functionKey = this.#webSocketRoutes.get(route) @@ -142,6 +161,7 @@ export default class WebSocketClients { ).create() clearTimeout(hardTimeout) + this._clearIdleTimeout(connectionId) this._processEvent( webSocketClient, @@ -159,6 +179,7 @@ export default class WebSocketClients { debugLog(`route:${route} on connection=${connectionId}`) const event = new WebSocketEvent(connectionId, route, message).create() + this._onWebSocketUsed(connectionId) this._processEvent(webSocketClient, connectionId, route, event) }) @@ -186,6 +207,7 @@ export default class WebSocketClients { const client = this._getWebSocketClient(connectionId) if (client) { + this._onWebSocketUsed(connectionId) client.send(payload) return true } From 0f68fa917b1dedf81538e45acb8359689b0ac8f0 Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 16:52:12 +0200 Subject: [PATCH 4/6] feat: add webSocketIdleTimeout option --- README.md | 1 + src/config/commandOptions.js | 4 ++++ src/config/defaultOptions.js | 1 + src/events/websocket/WebSocketClients.js | 5 ++--- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 989c8b5f2..2c9449b8d 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,7 @@ All CLI options are optional: --useWorkerThreads Uses worker threads for handlers. Requires node.js v11.7.0 or higher --websocketPort WebSocket port to listen on. Default: 3001 --webSocketHardTimeout Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours) +--webSocketIdleTimeout Set WebSocket idle timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 600 (10 minutes) --useDocker Run handlers in a docker container. ``` diff --git a/src/config/commandOptions.js b/src/config/commandOptions.js index 7035e9211..84dc7212f 100644 --- a/src/config/commandOptions.js +++ b/src/config/commandOptions.js @@ -82,6 +82,10 @@ export default { usage: 'Set WebSocket hard timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 7200 (2 hours)', }, + webSocketIdleTimeout: { + usage: + 'Set WebSocket idle timeout in seconds to reproduce AWS limits (https://docs.aws.amazon.com/apigateway/latest/developerguide/limits.html#apigateway-execution-service-websocket-limits-table). Default: 600 (10 minutes)', + }, useDocker: { usage: 'Uses docker for node/python/ruby', }, diff --git a/src/config/defaultOptions.js b/src/config/defaultOptions.js index 55c24b2ef..f6b8df877 100644 --- a/src/config/defaultOptions.js +++ b/src/config/defaultOptions.js @@ -23,6 +23,7 @@ export default { useWorkerThreads: false, websocketPort: 3001, webSocketHardTimeout: 7200, + webSocketIdleTimeout: 600, useDocker: false, functionCleanupIdleTimeSeconds: 60, } diff --git a/src/events/websocket/WebSocketClients.js b/src/events/websocket/WebSocketClients.js index f7d0b3aab..6d9d372ee 100644 --- a/src/events/websocket/WebSocketClients.js +++ b/src/events/websocket/WebSocketClients.js @@ -41,7 +41,6 @@ export default class WebSocketClients { this.#clients.delete(client) this.#clients.delete(connectionId) - this.#idleTimeouts.delete(connectionId, client) return connectionId } @@ -57,7 +56,7 @@ export default class WebSocketClients { const timeoutId = setTimeout(() => { debugLog(`timeout:idle:${connectionId}`) client.close(1001, 'Going away') - }, 10 * 1000) + }, this.#options.webSocketIdleTimeout * 1000) this.#idleTimeouts.set(client, timeoutId) } @@ -161,7 +160,7 @@ export default class WebSocketClients { ).create() clearTimeout(hardTimeout) - this._clearIdleTimeout(connectionId) + this._clearIdleTimeout(webSocketClient) this._processEvent( webSocketClient, From 374850e2e33f0f5ffc0d71999d690c3dc8a1195c Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 17:26:51 +0200 Subject: [PATCH 5/6] refactor: WebSocket hard timeout --- src/events/websocket/WebSocketClients.js | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/events/websocket/WebSocketClients.js b/src/events/websocket/WebSocketClients.js index 6d9d372ee..c17833957 100644 --- a/src/events/websocket/WebSocketClients.js +++ b/src/events/websocket/WebSocketClients.js @@ -21,6 +21,7 @@ export default class WebSocketClients { #webSocketRoutes = new Map() #websocketsApiRouteSelectionExpression = null #idleTimeouts = new WeakMap() + #hardTimeouts = new WeakMap() constructor(serverless, options, lambda) { this.#lambda = lambda @@ -34,6 +35,7 @@ export default class WebSocketClients { this.#clients.set(client, connectionId) this.#clients.set(connectionId, client) this._onWebSocketUsed(connectionId) + this._addHardTimeout(client, connectionId) } _removeWebSocketClient(client) { @@ -49,12 +51,26 @@ export default class WebSocketClients { return this.#clients.get(connectionId) } + _addHardTimeout(client, connectionId) { + const timeoutId = setTimeout(() => { + debugLog(`timeout:hard:${connectionId}`) + client.close(1001, 'Going away') + }, this.#options.webSocketHardTimeout * 1000) + this.#hardTimeouts.set(client, timeoutId) + } + + _clearHardTimeout(client) { + const timeoutId = this.#hardTimeouts.get(client) + clearTimeout(timeoutId) + } + _onWebSocketUsed(connectionId) { const client = this._getWebSocketClient(connectionId) this._clearIdleTimeout(client) + debugLog(`timeout:idle:${connectionId}:reset`) const timeoutId = setTimeout(() => { - debugLog(`timeout:idle:${connectionId}`) + debugLog(`timeout:idle:${connectionId}:trigger`) client.close(1001, 'Going away') }, this.#options.webSocketIdleTimeout * 1000) this.#idleTimeouts.set(client, timeoutId) @@ -145,11 +161,6 @@ export default class WebSocketClients { this._processEvent(webSocketClient, connectionId, '$connect', connectEvent) - const hardTimeout = setTimeout(() => { - debugLog(`timeout:hard:${connectionId}`) - webSocketClient.close() - }, this.#options.webSocketHardTimeout * 1000) - webSocketClient.on('close', () => { debugLog(`disconnect:${connectionId}`) @@ -159,7 +170,7 @@ export default class WebSocketClients { connectionId, ).create() - clearTimeout(hardTimeout) + this._clearHardTimeout(webSocketClient) this._clearIdleTimeout(webSocketClient) this._processEvent( From d8af57fecff0cfaffd84dc881fc5457beb0325e6 Mon Sep 17 00:00:00 2001 From: Moroine Bentefrit Date: Sun, 23 Aug 2020 17:27:40 +0200 Subject: [PATCH 6/6] fix: WebSocketContext connectedAt attribute --- .../lambda-events/WebSocketRequestContext.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/events/websocket/lambda-events/WebSocketRequestContext.js b/src/events/websocket/lambda-events/WebSocketRequestContext.js index 5ff978ef6..c666398b2 100644 --- a/src/events/websocket/lambda-events/WebSocketRequestContext.js +++ b/src/events/websocket/lambda-events/WebSocketRequestContext.js @@ -2,15 +2,28 @@ import { createUniqueId, formatToClfTime } from '../../../utils/index.js' const { now } = Date +const connectedAt = new Map() + export default class WebSocketRequestContext { #connectionId = null #eventType = null #route = null + #connectedAt = null constructor(eventType, route, connectionId) { this.#connectionId = connectionId this.#eventType = eventType this.#route = route + + if (eventType === 'CONNECT') { + connectedAt.set(connectionId, now()) + } + + this.#connectedAt = connectedAt.get(connectionId) + + if (eventType === 'DISCONNECT') { + connectedAt.delete(connectionId) + } } create() { @@ -18,7 +31,7 @@ export default class WebSocketRequestContext { const requestContext = { apiId: 'private', - connectedAt: now(), // TODO this is probably not correct, and should be the initial connection time? + connectedAt: this.#connectedAt, connectionId: this.#connectionId, domainName: 'localhost', eventType: this.#eventType,