You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
297 lines
8.3 KiB
JavaScript
297 lines
8.3 KiB
JavaScript
/*
|
|
* 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 errors = require('./errors');
|
|
const utils = require('./utils');
|
|
const types = require('./types');
|
|
const promiseUtils = require('./promise-utils');
|
|
|
|
/**
|
|
* Encapsulates the logic for dealing with the different prepare request and response flows, including failover when
|
|
* trying to prepare a query.
|
|
*/
|
|
class PrepareHandler {
|
|
/**
|
|
* Creates a new instance of PrepareHandler
|
|
* @param {Client} client
|
|
* @param {LoadBalancingPolicy} loadBalancing
|
|
*/
|
|
constructor(client, loadBalancing) {
|
|
this._client = client;
|
|
this._loadBalancing = loadBalancing;
|
|
this.logEmitter = client.options.logEmitter;
|
|
this.log = utils.log;
|
|
}
|
|
|
|
/**
|
|
* Gets the query id and metadata for a prepared statement, preparing it on
|
|
* single host or on all hosts depending on the options.
|
|
* @param {Client} client
|
|
* @param {LoadBalancingPolicy} loadBalancing
|
|
* @param {String} query
|
|
* @param {String} keyspace
|
|
* @returns {Promise<{queryId, meta}>}
|
|
* @static
|
|
*/
|
|
static async getPrepared(client, loadBalancing, query, keyspace) {
|
|
const info = client.metadata.getPreparedInfo(keyspace, query);
|
|
if (info.queryId) {
|
|
return info;
|
|
}
|
|
|
|
if (info.preparing) {
|
|
// It's already being prepared
|
|
return await promiseUtils.fromEvent(info, 'prepared');
|
|
}
|
|
|
|
const instance = new PrepareHandler(client, loadBalancing);
|
|
return await instance._prepare(info, query, keyspace);
|
|
}
|
|
|
|
/**
|
|
* @param {Client} client
|
|
* @param {LoadBalancingPolicy} loadBalancing
|
|
* @param {Array} queries
|
|
* @param {String} keyspace
|
|
* @static
|
|
*/
|
|
static async getPreparedMultiple(client, loadBalancing, queries, keyspace) {
|
|
const result = [];
|
|
|
|
for (const item of queries) {
|
|
let query;
|
|
|
|
if (item) {
|
|
query = typeof item === 'string' ? item : item.query;
|
|
}
|
|
|
|
if (typeof query !== 'string') {
|
|
throw new errors.ArgumentError('Query item should be a string');
|
|
}
|
|
|
|
const { queryId, meta } = await PrepareHandler.getPrepared(client, loadBalancing, query, keyspace);
|
|
result.push({ query, params: utils.adaptNamedParamsPrepared(item.params, meta.columns), queryId, meta });
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Prepares the query on a single host or on all hosts depending on the options.
|
|
* Uses the info 'prepared' event to emit the result.
|
|
* @param {Object} info
|
|
* @param {String} query
|
|
* @param {String} keyspace
|
|
* @returns {Promise<{queryId, meta}>}
|
|
*/
|
|
async _prepare(info, query, keyspace) {
|
|
info.preparing = true;
|
|
let iterator;
|
|
|
|
try {
|
|
iterator = await promiseUtils.newQueryPlan(this._loadBalancing, keyspace, null);
|
|
return await this._prepareWithQueryPlan(info, iterator, query, keyspace);
|
|
} catch (err) {
|
|
info.preparing = false;
|
|
err.query = query;
|
|
info.emit('prepared', err);
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uses the query plan to prepare the query on the first host and optionally on the rest of the hosts.
|
|
* @param {Object} info
|
|
* @param {Iterator} iterator
|
|
* @param {String} query
|
|
* @param {String} keyspace
|
|
* @returns {Promise<{queryId, meta}>}
|
|
* @private
|
|
*/
|
|
async _prepareWithQueryPlan(info, iterator, query, keyspace) {
|
|
const triedHosts = {};
|
|
|
|
while (true) {
|
|
const host = PrepareHandler.getNextHost(iterator, this._client.profileManager, triedHosts);
|
|
|
|
if (host === null) {
|
|
throw new errors.NoHostAvailableError(triedHosts);
|
|
}
|
|
|
|
try {
|
|
const connection = await PrepareHandler._borrowWithKeyspace(host, keyspace);
|
|
const response = await connection.prepareOnceAsync(query, keyspace);
|
|
|
|
if (this._client.options.prepareOnAllHosts) {
|
|
await this._prepareOnAllHosts(iterator, query, keyspace);
|
|
}
|
|
|
|
// Set the prepared metadata
|
|
info.preparing = false;
|
|
info.queryId = response.id;
|
|
info.meta = response.meta;
|
|
this._client.metadata.setPreparedById(info);
|
|
info.emit('prepared', null, info);
|
|
|
|
return info;
|
|
|
|
} catch (err) {
|
|
triedHosts[host.address] = err;
|
|
|
|
if (!err.isSocketError && !(err instanceof errors.OperationTimedOutError)) {
|
|
// There's no point in retrying syntax errors and other response errors
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the next host from the query plan.
|
|
* @param {Iterator} iterator
|
|
* @param {ProfileManager} profileManager
|
|
* @param {Object} [triedHosts]
|
|
* @return {Host|null}
|
|
*/
|
|
static getNextHost(iterator, profileManager, triedHosts) {
|
|
let host;
|
|
// Get a host that is UP in a sync loop
|
|
while (true) {
|
|
const item = iterator.next();
|
|
if (item.done) {
|
|
return null;
|
|
}
|
|
|
|
host = item.value;
|
|
|
|
// set the distance relative to the client first
|
|
const distance = profileManager.getDistance(host);
|
|
if (distance === types.distance.ignored) {
|
|
//If its marked as ignore by the load balancing policy, move on.
|
|
continue;
|
|
}
|
|
|
|
if (host.isUp()) {
|
|
break;
|
|
}
|
|
|
|
if (triedHosts) {
|
|
triedHosts[host.address] = 'Host considered as DOWN';
|
|
}
|
|
}
|
|
|
|
return host;
|
|
}
|
|
|
|
/**
|
|
* Prepares all queries on a single host.
|
|
* @param {Host} host
|
|
* @param {Array} allPrepared
|
|
*/
|
|
static async prepareAllQueries(host, allPrepared) {
|
|
const anyKeyspaceQueries = [];
|
|
|
|
const queriesByKeyspace = new Map();
|
|
allPrepared.forEach(info => {
|
|
let arr;
|
|
if (info.keyspace) {
|
|
arr = queriesByKeyspace.get(info.keyspace);
|
|
|
|
if (!arr) {
|
|
arr = [];
|
|
queriesByKeyspace.set(info.keyspace, arr);
|
|
}
|
|
} else {
|
|
arr = anyKeyspaceQueries;
|
|
}
|
|
|
|
arr.push(info.query);
|
|
});
|
|
|
|
for (const [keyspace, queries] of queriesByKeyspace) {
|
|
await PrepareHandler._borrowAndPrepare(host, keyspace, queries);
|
|
}
|
|
|
|
await PrepareHandler._borrowAndPrepare(host, null, anyKeyspaceQueries);
|
|
}
|
|
|
|
/**
|
|
* Borrows a connection from the host and prepares the queries provided.
|
|
* @param {Host} host
|
|
* @param {String} keyspace
|
|
* @param {Array} queries
|
|
* @returns {Promise<void>}
|
|
* @private
|
|
*/
|
|
static async _borrowAndPrepare(host, keyspace, queries) {
|
|
if (queries.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const connection = await PrepareHandler._borrowWithKeyspace(host, keyspace);
|
|
|
|
for (const query of queries) {
|
|
await connection.prepareOnceAsync(query, keyspace);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Borrows a connection and changes the active keyspace on the connection, if needed.
|
|
* It does not perform any retry or error handling.
|
|
* @param {Host!} host
|
|
* @param {string} keyspace
|
|
* @returns {Promise<Connection>}
|
|
* @throws {errors.BusyConnectionError} When the connection is busy.
|
|
* @throws {errors.ResponseError} For invalid keyspaces.
|
|
* @throws {Error} For socket errors.
|
|
* @private
|
|
*/
|
|
static async _borrowWithKeyspace(host, keyspace) {
|
|
const connection = host.borrowConnection();
|
|
|
|
if (keyspace && connection.keyspace !== keyspace) {
|
|
await connection.changeKeyspace(keyspace);
|
|
}
|
|
|
|
return connection;
|
|
}
|
|
|
|
/**
|
|
* Prepares the provided query on all hosts, except the host provided.
|
|
* @param {Iterator} iterator
|
|
* @param {String} query
|
|
* @param {String} keyspace
|
|
* @private
|
|
*/
|
|
_prepareOnAllHosts(iterator, query, keyspace) {
|
|
const queries = [ query ];
|
|
let h;
|
|
const hosts = [];
|
|
|
|
while ((h = PrepareHandler.getNextHost(iterator, this._client.profileManager)) !== null) {
|
|
hosts.push(h);
|
|
}
|
|
|
|
return Promise.all(hosts.map(h =>
|
|
PrepareHandler
|
|
._borrowAndPrepare(h, keyspace, queries)
|
|
.catch(err => this.log('verbose', `Unexpected error while preparing query (${query}) on ${h.address}`, err))));
|
|
}
|
|
}
|
|
|
|
module.exports = PrepareHandler; |