| /* |
| * Copyright DataStax, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| 'use strict'; |
| |
| const { Stream } = require('stream'); |
| const utils = require('../utils'); |
| |
| /** |
| * Utilities for concurrent query execution with the DataStax Node.js Driver. |
| * @module concurrent |
| */ |
| |
| /** |
| * Executes multiple queries concurrently at the defined concurrency level. |
| * @static |
| * @param {Client} client The {@link Client} instance. |
| * @param {String|Array<{query, params}>} query The query to execute per each parameter item. |
| * @param {Array<Array>|Stream|Object} parameters An {@link Array} or a readable {@link Stream} composed of {@link Array} |
| * items representing each individual set of parameters. Per each item in the {@link Array} or {@link Stream}, an |
| * execution is going to be made. |
| * @param {Object} [options] The execution options. |
| * @param {String} [options.executionProfile] The execution profile to be used. |
| * @param {Number} [options.concurrencyLevel=100] The concurrency level to determine the maximum amount of in-flight |
| * operations at any given time |
| * @param {Boolean} [options.raiseOnFirstError=true] Determines whether execution should stop after the first failed |
| * execution and the corresponding exception will be raised. |
| * @param {Boolean} [options.collectResults=false] Determines whether each individual |
| * [ResultSet]{@link module:types~ResultSet} instance should be collected in the grouped result. |
| * @param {Number} [options.maxErrors=100] The maximum amount of errors to be collected before ignoring the rest of |
| * the error results. |
| * @returns {Promise<ResultSetGroup>} A <code>Promise</code> of {@link ResultSetGroup} that is resolved when all the |
| * executions completed and it's rejected when <code>raiseOnFirstError</code> is <code>true</code> and there is one |
| * or more failures. |
| * @example <caption>Using a fixed query and an Array of Arrays as parameters</caption> |
| * const query = 'INSERT INTO table1 (id, value) VALUES (?, ?)'; |
| * const parameters = [[1, 'a'], [2, 'b'], [3, 'c'], ]; // ... |
| * const result = await executeConcurrent(client, query, parameters); |
| * @example <caption>Using a fixed query and a readable stream</caption> |
| * const stream = csvStream.pipe(transformLineToArrayStream); |
| * const result = await executeConcurrent(client, query, stream); |
| * @example <caption>Using a different queries</caption> |
| * const queryAndParameters = [ |
| * { query: 'INSERT INTO videos (id, name, user_id) VALUES (?, ?, ?)', |
| * params: [ id, name, userId ] }, |
| * { query: 'INSERT INTO user_videos (user_id, id, name) VALUES (?, ?, ?)', |
| * params: [ userId, id, name ] }, |
| * { query: 'INSERT INTO latest_videos (id, name, user_id) VALUES (?, ?, ?)', |
| * params: [ id, name, userId ] }, |
| * ]; |
| * |
| * const result = await executeConcurrent(client, queryAndParameters); |
| */ |
| function executeConcurrent(client, query, parameters, options) { |
| if (!client) { |
| throw new TypeError('Client instance is not defined'); |
| } |
| |
| if (typeof query === 'string') { |
| if (Array.isArray(parameters)) { |
| return new ArrayBasedExecutor(client, query, parameters, options).execute(); |
| } |
| |
| if (parameters instanceof Stream) { |
| return new StreamBasedExecutor(client, query, parameters, options).execute(); |
| } |
| |
| throw new TypeError('parameters should be an Array or a Stream instance'); |
| } |
| |
| if (Array.isArray(query)) { |
| options = parameters; |
| return new ArrayBasedExecutor(client, null, query, options).execute(); |
| } |
| |
| throw new TypeError('A string query or query and parameters array should be provided'); |
| } |
| |
| /** |
| * Wraps the functionality to execute given an Array. |
| * @ignore |
| */ |
| class ArrayBasedExecutor { |
| |
| /** |
| * @param {Client} client |
| * @param {String} query |
| * @param {Array<Array>|Array<{query, params}>} parameters |
| * @param {Object} [options] The execution options. |
| * @private |
| */ |
| constructor(client, query, parameters, options) { |
| this._client = client; |
| this._query = query; |
| this._parameters = parameters; |
| options = options || utils.emptyObject; |
| this._raiseOnFirstError = options.raiseOnFirstError !== false; |
| this._concurrencyLevel = Math.min(options.concurrencyLevel || 100, this._parameters.length); |
| this._queryOptions = { prepare: true, executionProfile: options.executionProfile }; |
| this._result = new ResultSetGroup(options); |
| this._stop = false; |
| } |
| |
| execute() { |
| const promises = new Array(this._concurrencyLevel); |
| |
| for (let i = 0; i < this._concurrencyLevel; i++) { |
| promises[i] = this._executeOneAtATime(i, 0); |
| } |
| |
| return Promise.all(promises).then(() => this._result); |
| } |
| |
| _executeOneAtATime(initialIndex, iteration) { |
| const index = initialIndex + this._concurrencyLevel * iteration; |
| |
| if (index >= this._parameters.length || this._stop) { |
| return Promise.resolve(); |
| } |
| |
| const item = this._parameters[index]; |
| let query; |
| let params; |
| |
| if (this._query === null) { |
| query = item.query; |
| params = item.params; |
| } else { |
| query = this._query; |
| params = item; |
| } |
| |
| return this._client.execute(query, params, this._queryOptions) |
| .then(rs => this._result.setResultItem(index, rs)) |
| .catch(err => this._setError(index, err)) |
| .then(() => this._executeOneAtATime(initialIndex, iteration + 1)); |
| } |
| |
| _setError(index, err) { |
| this._result.setError(index, err); |
| |
| if (this._raiseOnFirstError) { |
| this._stop = true; |
| throw err; |
| } |
| } |
| } |
| |
| /** |
| * Wraps the functionality to execute given a Stream. |
| * @ignore |
| */ |
| class StreamBasedExecutor { |
| |
| /** |
| * @param {Client} client |
| * @param {String} query |
| * @param {Stream} stream |
| * @param {Object} [options] The execution options. |
| * @private |
| */ |
| constructor(client, query, stream, options) { |
| this._client = client; |
| this._query = query; |
| this._stream = stream; |
| options = options || utils.emptyObject; |
| this._raiseOnFirstError = options.raiseOnFirstError !== false; |
| this._concurrencyLevel = options.concurrencyLevel || 100; |
| this._queryOptions = { prepare: true, executionProfile: options.executionProfile }; |
| this._inFlight = 0; |
| this._index = 0; |
| this._result = new ResultSetGroup(options); |
| this._resolveCallback = null; |
| this._rejectCallback = null; |
| this._readEnded = false; |
| } |
| |
| execute() { |
| return new Promise((resolve, reject) => { |
| this._resolveCallback = resolve; |
| this._rejectCallback = reject; |
| |
| this._stream |
| .on('data', params => this._executeOne(params)) |
| .on('error', err => this._setReadEnded(err)) |
| .on('end', () => this._setReadEnded()); |
| }); |
| } |
| |
| _executeOne(params) { |
| if (!Array.isArray(params)) { |
| return this._setReadEnded(new TypeError('Stream should be in objectMode and should emit Array instances')); |
| } |
| |
| if (this._readEnded) { |
| // Read ended abruptly because of incorrect format or error event being emitted. |
| // We shouldn't consider additional items. |
| return; |
| } |
| |
| const index = this._index++; |
| this._inFlight++; |
| |
| this._client.execute(this._query, params, this._queryOptions) |
| .then(rs => { |
| this._result.setResultItem(index, rs); |
| this._inFlight--; |
| }) |
| .catch(err => { |
| this._inFlight--; |
| this._setError(index, err); |
| }) |
| .then(() => { |
| if (this._stream.isPaused()) { |
| this._stream.resume(); |
| } |
| |
| if (this._readEnded && this._inFlight === 0) { |
| // When read ended and there are no more in-flight requests |
| // We yield the result to the user. |
| // It could have ended prematurely when there is a read error |
| // or there was an execution error and raiseOnFirstError is true |
| // In that case, calling the resolve callback has no effect |
| this._resolveCallback(this._result); |
| } |
| }); |
| |
| if (this._inFlight >= this._concurrencyLevel) { |
| this._stream.pause(); |
| } |
| } |
| |
| /** |
| * Marks the stream read process as ended. |
| * @param {Error} [err] The stream read error. |
| * @private |
| */ |
| _setReadEnded(err) { |
| if (!this._readEnded) { |
| this._readEnded = true; |
| |
| if (err) { |
| // There was an error while reading from the input stream. |
| // This should be surfaced as a failure |
| this._rejectCallback(err); |
| } else if (this._inFlight === 0) { |
| // Ended signaled and there are no more pending messages. |
| this._resolveCallback(this._result); |
| } |
| } |
| } |
| |
| _setError(index, err) { |
| this._result.setError(index, err); |
| |
| if (this._raiseOnFirstError) { |
| this._readEnded = true; |
| this._rejectCallback(err); |
| } |
| } |
| } |
| |
| /** |
| * Represents results from different related executions. |
| */ |
| class ResultSetGroup { |
| |
| /** |
| * Creates a new instance of {@link ResultSetGroup}. |
| * @ignore |
| */ |
| constructor(options) { |
| this._collectResults = options.collectResults; |
| this._maxErrors = options.maxErrors || 100; |
| this.totalExecuted = 0; |
| this.errors = []; |
| |
| if (this._collectResults) { |
| /** |
| * Gets an {@link Array} containing the [ResultSet]{@link module:types~ResultSet} instances from each execution. |
| * <p> |
| * Note that when <code>collectResults</code> is set to <code>false</code>, accessing this property will |
| * throw an error. |
| * </p> |
| * @type {Array} |
| */ |
| this.resultItems = []; |
| } else { |
| Object.defineProperty(this, 'resultItems', { enumerable: false, get: () => { |
| throw new Error('Property resultItems can not be accessed when collectResults is set to false'); |
| }}); |
| } |
| } |
| |
| /** @ignore */ |
| setResultItem(index, rs) { |
| this.totalExecuted++; |
| |
| if (this._collectResults) { |
| this.resultItems[index] = rs; |
| } |
| } |
| |
| /** |
| * Internal method to set the error of an execution. |
| * @ignore |
| */ |
| setError(index, err) { |
| this.totalExecuted++; |
| |
| if (this.errors.length < this._maxErrors) { |
| this.errors.push(err); |
| } |
| |
| if (this._collectResults) { |
| this.resultItems[index] = err; |
| } |
| } |
| } |
| |
| exports.executeConcurrent = executeConcurrent; |
| exports.ResultSetGroup = ResultSetGroup; |