From 5f39105219534cbe377b5bd2b9d1132cc610c06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antonio=20Barc=C3=A9los?= Date: Wed, 27 Dec 2023 11:10:33 +0100 Subject: [PATCH] Fix BrowserChannel close when is already closing WebSockets can take a while for closing and the `WebSocket.close` is called when status is `WS_CLOSING`, an error is thrown. This situation can happen when the driver is being closed while session still releasing connections back to the pool or after a receive timeout while closing the session. BrowserChannel should wait for the original close socket finish whenever a new close request happens to avoid this kind of error and provides a graceful shutdown for driver and session. --- .../src/channel/browser/browser-channel.js | 29 +++++++++------ .../channel/browser/browser-channel.test.js | 35 +++++++++++++++++++ .../channel/browser/browser-channel.js | 29 +++++++++------ 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/packages/bolt-connection/src/channel/browser/browser-channel.js b/packages/bolt-connection/src/channel/browser/browser-channel.js index 945783a04..4d7fed9e8 100644 --- a/packages/bolt-connection/src/channel/browser/browser-channel.js +++ b/packages/bolt-connection/src/channel/browser/browser-channel.js @@ -54,6 +54,7 @@ export default class WebSocketChannel { this._receiveTimeout = null this._receiveTimeoutStarted = false this._receiveTimeoutId = null + this._closingPromise = null const { scheme, error } = determineWebSocketScheme(config, protocolSupplier) if (error) { @@ -163,17 +164,23 @@ export default class WebSocketChannel { * @returns {Promise} A promise that will be resolved after channel is closed */ close () { - return new Promise((resolve, reject) => { - this._clearConnectionTimeout() - if (this._ws && this._ws.readyState !== WS_CLOSED) { - this._open = false - this.stopReceiveTimeout() - this._ws.onclose = () => resolve() - this._ws.close() - } else { - resolve() - } - }) + if (this._closingPromise === null) { + this._closingPromise = new Promise((resolve, reject) => { + this._clearConnectionTimeout() + if (this._ws && this._ws.readyState !== WS_CLOSED) { + this._open = false + this.stopReceiveTimeout() + this._ws.onclose = () => { + resolve() + } + this._ws.close() + } else { + resolve() + } + }) + } + + return this._closingPromise } /** diff --git a/packages/bolt-connection/test/channel/browser/browser-channel.test.js b/packages/bolt-connection/test/channel/browser/browser-channel.test.js index 4cc9c4dac..13451e043 100644 --- a/packages/bolt-connection/test/channel/browser/browser-channel.test.js +++ b/packages/bolt-connection/test/channel/browser/browser-channel.test.js @@ -323,6 +323,41 @@ describe('WebSocketChannel', () => { fakeSetTimeout.uninstall() } }) + + it('should return always the same promise', async () => { + const fakeSetTimeout = setTimeoutMock.install() + try { + // do not execute setTimeout callbacks + fakeSetTimeout.pause() + const address = ServerAddress.fromUrl('bolt://localhost:8989') + const driverConfig = { connectionTimeout: 4242 } + const channelConfig = new ChannelConfig( + address, + driverConfig, + SERVICE_UNAVAILABLE + ) + webSocketChannel = new WebSocketChannel( + channelConfig, + undefined, + createWebSocketFactory(WS_OPEN) + ) + + const promise1 = webSocketChannel.close() + const promise2 = webSocketChannel.close() + + expect(promise1).toBe(promise2) + + await Promise.all([promise1, promise2]) + + const promise3 = webSocketChannel.close() + + expect(promise3).toBe(promise2) + + await promise3 + } finally { + fakeSetTimeout.uninstall() + } + }) }) describe('.setupReceiveTimeout()', () => { diff --git a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js index 22f743cea..1124d49a0 100644 --- a/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js +++ b/packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js @@ -54,6 +54,7 @@ export default class WebSocketChannel { this._receiveTimeout = null this._receiveTimeoutStarted = false this._receiveTimeoutId = null + this._closingPromise = null const { scheme, error } = determineWebSocketScheme(config, protocolSupplier) if (error) { @@ -163,17 +164,23 @@ export default class WebSocketChannel { * @returns {Promise} A promise that will be resolved after channel is closed */ close () { - return new Promise((resolve, reject) => { - this._clearConnectionTimeout() - if (this._ws && this._ws.readyState !== WS_CLOSED) { - this._open = false - this.stopReceiveTimeout() - this._ws.onclose = () => resolve() - this._ws.close() - } else { - resolve() - } - }) + if (this._closingPromise === null) { + this._closingPromise = new Promise((resolve, reject) => { + this._clearConnectionTimeout() + if (this._ws && this._ws.readyState !== WS_CLOSED) { + this._open = false + this.stopReceiveTimeout() + this._ws.onclose = () => { + resolve() + } + this._ws.close() + } else { + resolve() + } + }) + } + + return this._closingPromise } /**