Skip to content

Commit 56d77c1

Browse files
authored
Fix BrowserChannel close when is already closing (#1169)
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.
1 parent ce8bc52 commit 56d77c1

File tree

3 files changed

+71
-22
lines changed

3 files changed

+71
-22
lines changed

packages/bolt-connection/src/channel/browser/browser-channel.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default class WebSocketChannel {
5454
this._receiveTimeout = null
5555
this._receiveTimeoutStarted = false
5656
this._receiveTimeoutId = null
57+
this._closingPromise = null
5758

5859
const { scheme, error } = determineWebSocketScheme(config, protocolSupplier)
5960
if (error) {
@@ -163,17 +164,23 @@ export default class WebSocketChannel {
163164
* @returns {Promise} A promise that will be resolved after channel is closed
164165
*/
165166
close () {
166-
return new Promise((resolve, reject) => {
167-
this._clearConnectionTimeout()
168-
if (this._ws && this._ws.readyState !== WS_CLOSED) {
169-
this._open = false
170-
this.stopReceiveTimeout()
171-
this._ws.onclose = () => resolve()
172-
this._ws.close()
173-
} else {
174-
resolve()
175-
}
176-
})
167+
if (this._closingPromise === null) {
168+
this._closingPromise = new Promise((resolve, reject) => {
169+
this._clearConnectionTimeout()
170+
if (this._ws && this._ws.readyState !== WS_CLOSED) {
171+
this._open = false
172+
this.stopReceiveTimeout()
173+
this._ws.onclose = () => {
174+
resolve()
175+
}
176+
this._ws.close()
177+
} else {
178+
resolve()
179+
}
180+
})
181+
}
182+
183+
return this._closingPromise
177184
}
178185

179186
/**

packages/bolt-connection/test/channel/browser/browser-channel.test.js

+35
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,41 @@ describe('WebSocketChannel', () => {
323323
fakeSetTimeout.uninstall()
324324
}
325325
})
326+
327+
it('should return always the same promise', async () => {
328+
const fakeSetTimeout = setTimeoutMock.install()
329+
try {
330+
// do not execute setTimeout callbacks
331+
fakeSetTimeout.pause()
332+
const address = ServerAddress.fromUrl('bolt://localhost:8989')
333+
const driverConfig = { connectionTimeout: 4242 }
334+
const channelConfig = new ChannelConfig(
335+
address,
336+
driverConfig,
337+
SERVICE_UNAVAILABLE
338+
)
339+
webSocketChannel = new WebSocketChannel(
340+
channelConfig,
341+
undefined,
342+
createWebSocketFactory(WS_OPEN)
343+
)
344+
345+
const promise1 = webSocketChannel.close()
346+
const promise2 = webSocketChannel.close()
347+
348+
expect(promise1).toBe(promise2)
349+
350+
await Promise.all([promise1, promise2])
351+
352+
const promise3 = webSocketChannel.close()
353+
354+
expect(promise3).toBe(promise2)
355+
356+
await promise3
357+
} finally {
358+
fakeSetTimeout.uninstall()
359+
}
360+
})
326361
})
327362

328363
describe('.setupReceiveTimeout()', () => {

packages/neo4j-driver-deno/lib/bolt-connection/channel/browser/browser-channel.js

+18-11
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default class WebSocketChannel {
5454
this._receiveTimeout = null
5555
this._receiveTimeoutStarted = false
5656
this._receiveTimeoutId = null
57+
this._closingPromise = null
5758

5859
const { scheme, error } = determineWebSocketScheme(config, protocolSupplier)
5960
if (error) {
@@ -163,17 +164,23 @@ export default class WebSocketChannel {
163164
* @returns {Promise} A promise that will be resolved after channel is closed
164165
*/
165166
close () {
166-
return new Promise((resolve, reject) => {
167-
this._clearConnectionTimeout()
168-
if (this._ws && this._ws.readyState !== WS_CLOSED) {
169-
this._open = false
170-
this.stopReceiveTimeout()
171-
this._ws.onclose = () => resolve()
172-
this._ws.close()
173-
} else {
174-
resolve()
175-
}
176-
})
167+
if (this._closingPromise === null) {
168+
this._closingPromise = new Promise((resolve, reject) => {
169+
this._clearConnectionTimeout()
170+
if (this._ws && this._ws.readyState !== WS_CLOSED) {
171+
this._open = false
172+
this.stopReceiveTimeout()
173+
this._ws.onclose = () => {
174+
resolve()
175+
}
176+
this._ws.close()
177+
} else {
178+
resolve()
179+
}
180+
})
181+
}
182+
183+
return this._closingPromise
177184
}
178185

179186
/**

0 commit comments

Comments
 (0)