diff --git a/lib/control-connection.js b/lib/control-connection.js index fd707529..5abf8b06 100644 --- a/lib/control-connection.js +++ b/lib/control-connection.js @@ -87,6 +87,7 @@ class ControlConnection extends events.EventEmitter { this._addressTranslator = this.options.policies.addressResolution; this._reconnectionPolicy = this.options.policies.reconnection; this._reconnectionSchedule = this._reconnectionPolicy.newSchedule(); + this._refreshInProgress = false; this._isShuttingDown = false; // Reference to the encoder of the last valid connection @@ -216,7 +217,6 @@ class ControlConnection extends events.EventEmitter { _setHealthListeners(host, connection) { const self = this; - let wasRefreshCalled = 0; function removeListeners() { host.removeListener('down', downOrIgnoredHandler); @@ -225,11 +225,6 @@ class ControlConnection extends events.EventEmitter { } function startReconnecting(hostDown) { - if (wasRefreshCalled++ !== 0) { - // Prevent multiple calls to reconnect - return; - } - removeListeners(); if (self._isShuttingDown) { @@ -446,13 +441,31 @@ class ControlConnection extends events.EventEmitter { } /** - * Acquires a new connection and refreshes topology and keyspace metadata. + * Acquires a new connection and refreshes topology and keyspace metadata, with protection against concurrent refreshes. *

When it fails obtaining a connection and there aren't any more hosts, it schedules reconnection.

*

When it fails obtaining the metadata, it marks connection and/or host unusable and retries using the same * iterator from query plan / host list

* @param {Iterator} [hostIterator] */ async _refresh(hostIterator) { + if (this._refreshInProgress) { + return; + } + this._refreshInProgress = true; + + try { + return await this._unsafeDoRefresh(hostIterator); + } finally { + this._refreshInProgress = false; + } + } + + /** + * The actual implementation of the refresh logic, without protection against concurrent executions. + *

Should only be used via _refresh.

+ * @param {Iterator} [hostIterator] + */ + async _unsafeDoRefresh(hostIterator) { if (this._isShuttingDown) { this.log('info', 'The ControlConnection will not be refreshed as the Client is being shutdown'); return; @@ -499,7 +512,7 @@ class ControlConnection extends events.EventEmitter { } // Retry the whole thing with the same query plan - return await this._refresh(hostIterator); + return await this._unsafeDoRefresh(hostIterator); } this._reconnectionSchedule = this._reconnectionPolicy.newSchedule(); diff --git a/lib/promise-utils.js b/lib/promise-utils.js index 5d4c98a2..d8aaf80a 100644 --- a/lib/promise-utils.js +++ b/lib/promise-utils.js @@ -145,11 +145,13 @@ function times(count, limit, fn) { /** * Deals with unexpected rejections in order to avoid the unhandled promise rejection warning or failure. - * @param {Promise} promise + * @param {Promise | undefined} promise * @returns {undefined} */ function toBackground(promise) { - promise.catch(() => {}); + if (promise) { + promise.catch(() => {}); + } } /** diff --git a/test/integration/short/control-connection-tests.js b/test/integration/short/control-connection-tests.js index b4b77cfc..a190e078 100644 --- a/test/integration/short/control-connection-tests.js +++ b/test/integration/short/control-connection-tests.js @@ -153,6 +153,25 @@ describe('ControlConnection', function () { assert.strictEqual(cc.hosts.length, 1); }); + it('should not break when refreshing concurrently', async () => { + const cc = newInstance(); + cc.options.policies.loadBalancing = new policies.loadBalancing.RoundRobinPolicy(); + disposeAfter(cc); + + await cc.init(); + await new Promise(r => cc.options.policies.loadBalancing.init(null, cc.hosts, r)); + + const refreshPromises = []; + // randomly emit cc._refresh 100 times + for (let i = 0; i < 100; i++) { + refreshPromises.push(cc._refresh()); + await helper.delayAsync(~~(Math.random() * 100)); + } + await Promise.all(refreshPromises); + assert.ok(cc.host); + assert.ok(cc.connection); + }); + it('should reconnect when host used goes down', async () => { const options = clientOptions.extend( utils.extend({ pooling: helper.getPoolingOptions(1, 1, 500) }, helper.baseOptions));