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.

312 lines
9.0 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 util = require('util');
const errors = require('./errors');
const types = require('./types');
const utils = require('./utils');
const RequestExecution = require('./request-execution');
const promiseUtils = require('./promise-utils');
/**
* Handles a BATCH, QUERY and EXECUTE request to the server, dealing with host fail-over and retries on error
*/
class RequestHandler {
/**
* Creates a new instance of RequestHandler.
* @param {Request} request
* @param {ExecutionOptions} execOptions
* @param {Client} client Client instance used to retrieve and set the keyspace.
*/
constructor(request, execOptions, client) {
this.client = client;
this._speculativeExecutionPlan = client.options.policies.speculativeExecution.newPlan(
client.keyspace, request.query || request.queries);
this.logEmitter = client.options.logEmitter;
this.log = utils.log;
this.request = request;
this.executionOptions = execOptions;
this.stackContainer = null;
this.triedHosts = {};
// start at -1 as first request does not count.
this.speculativeExecutions = -1;
this._hostIterator = null;
this._resolveCallback = null;
this._rejectCallback = null;
this._newExecutionTimeout = null;
/** @type {RequestExecution[]} */
this._executions = [];
}
/**
* Sends a new BATCH, QUERY or EXECUTE request.
* @param {Request} request
* @param {ExecutionOptions} execOptions
* @param {Client} client Client instance used to retrieve and set the keyspace.
* @returns {Promise<ResultSet>}
*/
static send(request, execOptions, client) {
const instance = new RequestHandler(request, execOptions, client);
return instance.send();
}
/**
* Gets a connection from the next host according to the query plan or throws a NoHostAvailableError.
* @returns {{host, connection}}
* @throws {NoHostAvailableError}
*/
getNextConnection() {
let host;
let connection;
const iterator = this._hostIterator;
// Get a host that is UP in a sync loop
while (true) {
const item = iterator.next();
if (item.done) {
throw new errors.NoHostAvailableError(this.triedHosts);
}
host = item.value;
// Set the distance relative to the client first
const distance = this.client.profileManager.getDistance(host);
if (distance === types.distance.ignored) {
//If its marked as ignore by the load balancing policy, move on.
continue;
}
if (!host.isUp()) {
this.triedHosts[host.address] = 'Host considered as DOWN';
continue;
}
try {
connection = host.borrowConnection();
this.triedHosts[host.address] = null;
break;
} catch (err) {
this.triedHosts[host.address] = err;
}
}
return { connection, host };
}
/**
* Gets an available connection and sends the request
* @returns {Promise<ResultSet>}
*/
send() {
if (this.executionOptions.getCaptureStackTrace()) {
Error.captureStackTrace(this.stackContainer = {});
}
return new Promise((resolve, reject) => {
this._resolveCallback = resolve;
this._rejectCallback = reject;
const lbp = this.executionOptions.getLoadBalancingPolicy();
const fixedHost = this.executionOptions.getFixedHost();
if (fixedHost) {
// if host is configured bypass load balancing policy and use
// a single host plan.
this._hostIterator = utils.arrayIterator([fixedHost]);
promiseUtils.toBackground(this._startNewExecution());
} else {
lbp.newQueryPlan(this.client.keyspace, this.executionOptions, (err, iterator) => {
if (err) {
return reject(err);
}
this._hostIterator = iterator;
promiseUtils.toBackground(this._startNewExecution());
});
}
});
}
/**
* Starts a new execution on the next host of the query plan.
* @param {Boolean} [isSpecExec]
* @returns {Promise<void>}
* @private
*/
async _startNewExecution(isSpecExec) {
if (isSpecExec) {
this.client.metrics.onSpeculativeExecution();
}
let host;
let connection;
try {
({ host, connection } = this.getNextConnection());
} catch (err) {
return this.handleNoHostAvailable(err, null);
}
if (isSpecExec && this._executions.length >= 0 && this._executions[0].wasCancelled()) {
// This method was called on the next tick and could not be cleared, the previous execution was cancelled so
// there's no point in launching a new execution.
return;
}
if (this.client.keyspace && this.client.keyspace !== connection.keyspace) {
try {
await connection.changeKeyspace(this.client.keyspace);
} catch (err) {
this.triedHosts[host.address] = err;
// The error occurred asynchronously
// We can blindly re-try to obtain a different host/connection.
return this._startNewExecution(isSpecExec);
}
}
const execution = new RequestExecution(this, host, connection);
this._executions.push(execution);
execution.start();
if (this.executionOptions.isIdempotent()) {
this._scheduleSpeculativeExecution(host);
}
}
/**
* Schedules next speculative execution, if any.
* @param {Host!} host
* @private
*/
_scheduleSpeculativeExecution(host) {
const delay = this._speculativeExecutionPlan.nextExecution(host);
if (typeof delay !== 'number' || delay < 0) {
return;
}
if (delay === 0) {
// Parallel speculative execution
return process.nextTick(() => {
promiseUtils.toBackground(this._startNewExecution(true));
});
}
// Create timer for speculative execution
this._newExecutionTimeout = setTimeout(() =>
promiseUtils.toBackground(this._startNewExecution(true)), delay);
}
/**
* Sets the keyspace in any connection that is already opened.
* @param {Client} client
* @returns {Promise}
*/
static setKeyspace(client) {
let connection;
for (const host of client.hosts.values()) {
connection = host.getActiveConnection();
if (connection) {
break;
}
}
if (!connection) {
throw new errors.DriverInternalError('No active connection found');
}
return connection.changeKeyspace(client.keyspace);
}
/**
* @param {Error} err
* @param {ResultSet} [result]
*/
setCompleted(err, result) {
if (this._newExecutionTimeout !== null) {
clearTimeout(this._newExecutionTimeout);
}
// Mark all executions as cancelled
for (const execution of this._executions) {
execution.cancel();
}
if (err) {
if (this.executionOptions.getCaptureStackTrace()) {
utils.fixStack(this.stackContainer.stack, err);
}
// Reject the promise
return this._rejectCallback(err);
}
if (result.info.warnings) {
// Log the warnings from the response
result.info.warnings.forEach(function (message, i, warnings) {
this.log('warning', util.format(
'Received warning (%d of %d) "%s" for "%s"',
i + 1,
warnings.length,
message,
this.request.query || 'batch'));
}, this);
}
// We used to invoke the callback on next tick to allow stack unwinding and prevent the optimizing compiler to
// optimize read and write functions together.
// As we are resolving a Promise then() and catch() are always scheduled in the microtask queue
// We can invoke the resolve method directly.
this._resolveCallback(result);
}
/**
* @param {NoHostAvailableError} err
* @param {RequestExecution|null} execution
*/
handleNoHostAvailable(err, execution) {
if (execution !== null) {
// Remove the execution
const index = this._executions.indexOf(execution);
this._executions.splice(index, 1);
}
if (this._executions.length === 0) {
// There aren't any other executions, we should report back to the user that there isn't
// a host available for executing the request
this.setCompleted(err);
}
}
/**
* Gets a long lived closure that can fetch the next page.
* @returns {Function}
*/
getNextPageHandler() {
const request = this.request;
const execOptions = this.executionOptions;
const client = this.client;
return function nextPageHandler(pageState) {
execOptions.setPageState(pageState);
return new RequestHandler(request, execOptions, client).send();
};
}
}
module.exports = RequestHandler;