Node JS version
This commit is contained in:
883
node_modules/cassandra-driver/lib/policies/load-balancing.js
generated
vendored
Normal file
883
node_modules/cassandra-driver/lib/policies/load-balancing.js
generated
vendored
Normal file
@@ -0,0 +1,883 @@
|
||||
/*
|
||||
* 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 types = require('../types');
|
||||
const utils = require('../utils.js');
|
||||
const errors = require('../errors.js');
|
||||
|
||||
const doneIteratorObject = Object.freeze({ done: true });
|
||||
const newlyUpInterval = 60000;
|
||||
|
||||
/** @module policies/loadBalancing */
|
||||
/**
|
||||
* Base class for Load Balancing Policies
|
||||
* @constructor
|
||||
*/
|
||||
function LoadBalancingPolicy() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the load balancing policy, called after the driver obtained the information of the cluster.
|
||||
* @param {Client} client
|
||||
* @param {HostMap} hosts
|
||||
* @param {Function} callback
|
||||
*/
|
||||
LoadBalancingPolicy.prototype.init = function (client, hosts, callback) {
|
||||
this.client = client;
|
||||
this.hosts = hosts;
|
||||
callback();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the distance assigned by this policy to the provided host.
|
||||
* @param {Host} host
|
||||
*/
|
||||
LoadBalancingPolicy.prototype.getDistance = function (host) {
|
||||
return types.distance.local;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an iterator with the hosts for a new query.
|
||||
* Each new query will call this method. The first host in the result will
|
||||
* then be used to perform the query.
|
||||
* @param {String} keyspace Name of currently logged keyspace at <code>Client</code> level.
|
||||
* @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
|
||||
* @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
|
||||
* second parameter.
|
||||
*/
|
||||
LoadBalancingPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
|
||||
callback(new Error('You must implement a query plan for the LoadBalancingPolicy class'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an associative array containing the policy options.
|
||||
*/
|
||||
LoadBalancingPolicy.prototype.getOptions = function () {
|
||||
return new Map();
|
||||
};
|
||||
|
||||
/**
|
||||
* This policy yield nodes in a round-robin fashion.
|
||||
* @extends LoadBalancingPolicy
|
||||
* @constructor
|
||||
*/
|
||||
function RoundRobinPolicy() {
|
||||
this.index = 0;
|
||||
}
|
||||
|
||||
util.inherits(RoundRobinPolicy, LoadBalancingPolicy);
|
||||
|
||||
/**
|
||||
* Returns an iterator with the hosts to be used as coordinator for a query.
|
||||
* @param {String} keyspace Name of currently logged keyspace at <code>Client</code> level.
|
||||
* @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
|
||||
* @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
|
||||
* second parameter.
|
||||
*/
|
||||
RoundRobinPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
|
||||
if (!this.hosts) {
|
||||
return callback(new Error('Load balancing policy not initialized'));
|
||||
}
|
||||
const hosts = this.hosts.values();
|
||||
const self = this;
|
||||
let counter = 0;
|
||||
|
||||
let planIndex = self.index % hosts.length;
|
||||
self.index += 1;
|
||||
if (self.index >= utils.maxInt) {
|
||||
self.index = 0;
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
next: function () {
|
||||
if (++counter > hosts.length) {
|
||||
return doneIteratorObject;
|
||||
}
|
||||
return {value: hosts[planIndex++ % hosts.length], done: false};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* A data-center aware Round-robin load balancing policy.
|
||||
* This policy provides round-robin queries over the nodes of the local
|
||||
* data center.
|
||||
* @param {?String} [localDc] local datacenter name. This value overrides the 'localDataCenter' Client option \
|
||||
* and is useful for cases where you have multiple execution profiles that you intend on using for routing
|
||||
* requests to different data centers.
|
||||
* @extends {LoadBalancingPolicy}
|
||||
* @constructor
|
||||
*/
|
||||
function DCAwareRoundRobinPolicy(localDc) {
|
||||
this.localDc = localDc;
|
||||
this.index = 0;
|
||||
/** @type {Array} */
|
||||
this.localHostsArray = null;
|
||||
}
|
||||
|
||||
util.inherits(DCAwareRoundRobinPolicy, LoadBalancingPolicy);
|
||||
|
||||
/**
|
||||
* Initializes the load balancing policy.
|
||||
* @param {Client} client
|
||||
* @param {HostMap} hosts
|
||||
* @param {Function} callback
|
||||
*/
|
||||
DCAwareRoundRobinPolicy.prototype.init = function (client, hosts, callback) {
|
||||
this.client = client;
|
||||
this.hosts = hosts;
|
||||
hosts.on('add', this._cleanHostCache.bind(this));
|
||||
hosts.on('remove', this._cleanHostCache.bind(this));
|
||||
|
||||
try {
|
||||
setLocalDc(this, client, this.hosts);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the distance depending on the datacenter.
|
||||
* @param {Host} host
|
||||
*/
|
||||
DCAwareRoundRobinPolicy.prototype.getDistance = function (host) {
|
||||
if (host.datacenter === this.localDc) {
|
||||
return types.distance.local;
|
||||
}
|
||||
|
||||
return types.distance.ignored;
|
||||
};
|
||||
|
||||
DCAwareRoundRobinPolicy.prototype._cleanHostCache = function () {
|
||||
this.localHostsArray = null;
|
||||
};
|
||||
|
||||
DCAwareRoundRobinPolicy.prototype._resolveLocalHosts = function() {
|
||||
const hosts = this.hosts.values();
|
||||
if (this.localHostsArray) {
|
||||
//there were already calculated
|
||||
return;
|
||||
}
|
||||
this.localHostsArray = [];
|
||||
hosts.forEach(function (h) {
|
||||
if (!h.datacenter) {
|
||||
//not a remote dc node
|
||||
return;
|
||||
}
|
||||
if (h.datacenter === this.localDc) {
|
||||
this.localHostsArray.push(h);
|
||||
}
|
||||
}, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* It returns an iterator that yields local nodes.
|
||||
* @param {String} keyspace Name of currently logged keyspace at <code>Client</code> level.
|
||||
* @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
|
||||
* @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
|
||||
* second parameter.
|
||||
*/
|
||||
DCAwareRoundRobinPolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
|
||||
if (!this.hosts) {
|
||||
return callback(new Error('Load balancing policy not initialized'));
|
||||
}
|
||||
this.index += 1;
|
||||
if (this.index >= utils.maxInt) {
|
||||
this.index = 0;
|
||||
}
|
||||
this._resolveLocalHosts();
|
||||
// Use a local reference of hosts
|
||||
const localHostsArray = this.localHostsArray;
|
||||
let planLocalIndex = this.index;
|
||||
let counter = 0;
|
||||
callback(null, {
|
||||
next: function () {
|
||||
let host;
|
||||
if (counter++ < localHostsArray.length) {
|
||||
host = localHostsArray[planLocalIndex++ % localHostsArray.length];
|
||||
return { value: host, done: false };
|
||||
}
|
||||
return doneIteratorObject;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an associative array containing the policy options.
|
||||
*/
|
||||
DCAwareRoundRobinPolicy.prototype.getOptions = function () {
|
||||
return new Map([
|
||||
['localDataCenter', this.localDc ]
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* A wrapper load balancing policy that add token awareness to a child policy.
|
||||
* @param {LoadBalancingPolicy} childPolicy
|
||||
* @extends LoadBalancingPolicy
|
||||
* @constructor
|
||||
*/
|
||||
function TokenAwarePolicy (childPolicy) {
|
||||
if (!childPolicy) {
|
||||
throw new Error("You must specify a child load balancing policy");
|
||||
}
|
||||
this.childPolicy = childPolicy;
|
||||
}
|
||||
|
||||
util.inherits(TokenAwarePolicy, LoadBalancingPolicy);
|
||||
|
||||
TokenAwarePolicy.prototype.init = function (client, hosts, callback) {
|
||||
this.client = client;
|
||||
this.hosts = hosts;
|
||||
this.childPolicy.init(client, hosts, callback);
|
||||
};
|
||||
|
||||
TokenAwarePolicy.prototype.getDistance = function (host) {
|
||||
return this.childPolicy.getDistance(host);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the hosts to use for a new query.
|
||||
* The returned plan will return local replicas first, if replicas can be determined, followed by the plan of the
|
||||
* child policy.
|
||||
* @param {String} keyspace Name of currently logged keyspace at <code>Client</code> level.
|
||||
* @param {ExecutionOptions|null} executionOptions The information related to the execution of the request.
|
||||
* @param {Function} callback The function to be invoked with the error as first parameter and the host iterator as
|
||||
* second parameter.
|
||||
*/
|
||||
TokenAwarePolicy.prototype.newQueryPlan = function (keyspace, executionOptions, callback) {
|
||||
let routingKey;
|
||||
if (executionOptions) {
|
||||
routingKey = executionOptions.getRoutingKey();
|
||||
if (executionOptions.getKeyspace()) {
|
||||
keyspace = executionOptions.getKeyspace();
|
||||
}
|
||||
}
|
||||
let replicas;
|
||||
if (routingKey) {
|
||||
replicas = this.client.getReplicas(keyspace, routingKey);
|
||||
}
|
||||
if (!routingKey || !replicas) {
|
||||
return this.childPolicy.newQueryPlan(keyspace, executionOptions, callback);
|
||||
}
|
||||
const iterator = new TokenAwareIterator(keyspace, executionOptions, replicas, this.childPolicy);
|
||||
iterator.iterate(callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* An iterator that holds the context for the subsequent next() calls
|
||||
* @param {String} keyspace
|
||||
* @param {ExecutionOptions} execOptions
|
||||
* @param {Array} replicas
|
||||
* @param childPolicy
|
||||
* @constructor
|
||||
* @ignore
|
||||
*/
|
||||
function TokenAwareIterator(keyspace, execOptions, replicas, childPolicy) {
|
||||
this.keyspace = keyspace;
|
||||
this.childPolicy = childPolicy;
|
||||
this.options = execOptions;
|
||||
this.localReplicas = [];
|
||||
this.replicaIndex = 0;
|
||||
this.replicaMap = {};
|
||||
this.childIterator = null;
|
||||
// Memoize the local replicas
|
||||
// The amount of local replicas should be defined before start iterating, in order to select an
|
||||
// appropriate (pseudo random) startIndex
|
||||
for (let i = 0; i < replicas.length; i++) {
|
||||
const host = replicas[i];
|
||||
if (this.childPolicy.getDistance(host) !== types.distance.local) {
|
||||
continue;
|
||||
}
|
||||
this.replicaMap[host.address] = true;
|
||||
this.localReplicas.push(host);
|
||||
}
|
||||
// We use a PRNG to set the replica index
|
||||
// We only care about proportional fair scheduling between replicas of a given token
|
||||
// Math.random() has an extremely short permutation cycle length but we don't care about collisions
|
||||
this.startIndex = Math.floor(Math.random() * this.localReplicas.length);
|
||||
}
|
||||
|
||||
TokenAwareIterator.prototype.iterate = function (callback) {
|
||||
//Load the child policy hosts
|
||||
const self = this;
|
||||
this.childPolicy.newQueryPlan(this.keyspace, this.options, function (err, iterator) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
//get the iterator of the child policy in case is needed
|
||||
self.childIterator = iterator;
|
||||
callback(null, {
|
||||
next: function () { return self.computeNext(); }
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
TokenAwareIterator.prototype.computeNext = function () {
|
||||
let host;
|
||||
if (this.replicaIndex < this.localReplicas.length) {
|
||||
host = this.localReplicas[(this.startIndex + (this.replicaIndex++)) % this.localReplicas.length];
|
||||
return { value: host, done: false };
|
||||
}
|
||||
// Return hosts from child policy
|
||||
let item;
|
||||
while ((item = this.childIterator.next()) && !item.done) {
|
||||
if (this.replicaMap[item.value.address]) {
|
||||
// Avoid yielding local replicas from the child load balancing policy query plan
|
||||
continue;
|
||||
}
|
||||
return item;
|
||||
}
|
||||
return doneIteratorObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an associative array containing the policy options.
|
||||
*/
|
||||
TokenAwarePolicy.prototype.getOptions = function () {
|
||||
const map = new Map([
|
||||
['childPolicy', this.childPolicy.constructor !== undefined ? this.childPolicy.constructor.name : null ]
|
||||
]);
|
||||
|
||||
if (this.childPolicy instanceof DCAwareRoundRobinPolicy) {
|
||||
map.set('localDataCenter', this.childPolicy.localDc);
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new policy that wraps the provided child policy but only "allow" hosts
|
||||
* from the provided list.
|
||||
* @class
|
||||
* @classdesc
|
||||
* A load balancing policy wrapper that ensure that only hosts from a provided
|
||||
* allow list will ever be returned.
|
||||
* <p>
|
||||
* This policy wraps another load balancing policy and will delegate the choice
|
||||
* of hosts to the wrapped policy with the exception that only hosts contained
|
||||
* in the allow list provided when constructing this policy will ever be
|
||||
* returned. Any host not in the while list will be considered ignored
|
||||
* and thus will not be connected to.
|
||||
* <p>
|
||||
* This policy can be useful to ensure that the driver only connects to a
|
||||
* predefined set of hosts. Keep in mind however that this policy defeats
|
||||
* somewhat the host auto-detection of the driver. As such, this policy is only
|
||||
* useful in a few special cases or for testing, but is not optimal in general.
|
||||
* If all you want to do is limiting connections to hosts of the local
|
||||
* data-center then you should use DCAwareRoundRobinPolicy and *not* this policy
|
||||
* in particular.
|
||||
* @param {LoadBalancingPolicy} childPolicy the wrapped policy.
|
||||
* @param {Array.<string>} allowList The hosts address in the format ipAddress:port.
|
||||
* Only hosts from this list may get connected
|
||||
* to (whether they will get connected to or not depends on the child policy).
|
||||
* @extends LoadBalancingPolicy
|
||||
* @constructor
|
||||
*/
|
||||
function AllowListPolicy (childPolicy, allowList) {
|
||||
if (!childPolicy) {
|
||||
throw new Error("You must specify a child load balancing policy");
|
||||
}
|
||||
if (!Array.isArray(allowList)) {
|
||||
throw new Error("You must provide the list of allowed host addresses");
|
||||
}
|
||||
|
||||
this.childPolicy = childPolicy;
|
||||
this.allowList = new Map(allowList.map(address => [ address, true ]));
|
||||
}
|
||||
|
||||
util.inherits(AllowListPolicy, LoadBalancingPolicy);
|
||||
|
||||
AllowListPolicy.prototype.init = function (client, hosts, callback) {
|
||||
this.childPolicy.init(client, hosts, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Uses the child policy to return the distance to the host if included in the allow list.
|
||||
* Any host not in the while list will be considered ignored.
|
||||
* @param host
|
||||
*/
|
||||
AllowListPolicy.prototype.getDistance = function (host) {
|
||||
if (!this._contains(host)) {
|
||||
return types.distance.ignored;
|
||||
}
|
||||
return this.childPolicy.getDistance(host);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {Host} host
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
AllowListPolicy.prototype._contains = function (host) {
|
||||
return !!this.allowList.get(host.address);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the hosts to use for a new query filtered by the allow list.
|
||||
*/
|
||||
AllowListPolicy.prototype.newQueryPlan = function (keyspace, info, callback) {
|
||||
const self = this;
|
||||
this.childPolicy.newQueryPlan(keyspace, info, function (err, iterator) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, self._filter(iterator));
|
||||
});
|
||||
};
|
||||
|
||||
AllowListPolicy.prototype._filter = function (childIterator) {
|
||||
const self = this;
|
||||
return {
|
||||
next: function () {
|
||||
const item = childIterator.next();
|
||||
if (!item.done && !self._contains(item.value)) {
|
||||
return this.next();
|
||||
}
|
||||
return item;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets an associative array containing the policy options.
|
||||
*/
|
||||
AllowListPolicy.prototype.getOptions = function () {
|
||||
return new Map([
|
||||
['childPolicy', this.childPolicy.constructor !== undefined ? this.childPolicy.constructor.name : null ],
|
||||
['allowList', Array.from(this.allowList.keys())]
|
||||
]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new instance of the policy.
|
||||
* @classdesc
|
||||
* Exposed for backward-compatibility only, it's recommended that you use {@link AllowListPolicy} instead.
|
||||
* @param {LoadBalancingPolicy} childPolicy the wrapped policy.
|
||||
* @param {Array.<string>} allowList The hosts address in the format ipAddress:port.
|
||||
* Only hosts from this list may get connected to (whether they will get connected to or not depends on the child
|
||||
* policy).
|
||||
* @extends AllowListPolicy
|
||||
* @deprecated Use allow-list instead. It will be removed in future major versions.
|
||||
* @constructor
|
||||
*/
|
||||
function WhiteListPolicy(childPolicy, allowList) {
|
||||
AllowListPolicy.call(this, childPolicy, allowList);
|
||||
}
|
||||
|
||||
util.inherits(WhiteListPolicy, AllowListPolicy);
|
||||
|
||||
/**
|
||||
* A load-balancing policy implementation that attempts to fairly distribute the load based on the amount of in-flight
|
||||
* request per hosts. The local replicas are initially shuffled and
|
||||
* <a href="https://www.eecs.harvard.edu/~michaelm/postscripts/mythesis.pdf">between the first two nodes in the
|
||||
* shuffled list, the one with fewer in-flight requests is selected as coordinator</a>.
|
||||
*
|
||||
* <p>
|
||||
* Additionally, it detects unresponsive replicas and reorders them at the back of the query plan.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* For graph analytics queries, it uses the preferred analytics graph server previously obtained by driver as first
|
||||
* host in the query plan.
|
||||
* </p>
|
||||
*/
|
||||
class DefaultLoadBalancingPolicy extends LoadBalancingPolicy {
|
||||
|
||||
/**
|
||||
* Creates a new instance of <code>DefaultLoadBalancingPolicy</code>.
|
||||
* @param {String|Object} [options] The local data center name or the optional policy options object.
|
||||
* <p>
|
||||
* Note that when providing the local data center name, it overrides <code>localDataCenter</code> option at
|
||||
* <code>Client</code> level.
|
||||
* </p>
|
||||
* @param {String} [options.localDc] local data center name. This value overrides the 'localDataCenter' Client option
|
||||
* and is useful for cases where you have multiple execution profiles that you intend on using for routing
|
||||
* requests to different data centers.
|
||||
* @param {Function} [options.filter] A function to apply to determine if hosts are included in the query plan.
|
||||
* The function takes a Host parameter and returns a Boolean.
|
||||
*/
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
if (typeof options === 'string') {
|
||||
options = { localDc: options };
|
||||
} else if (!options) {
|
||||
options = utils.emptyObject;
|
||||
}
|
||||
|
||||
this._client = null;
|
||||
this._hosts = null;
|
||||
this._filteredHosts = null;
|
||||
this._preferredHost = null;
|
||||
this._index = 0;
|
||||
this.localDc = options.localDc;
|
||||
this._filter = options.filter || this._defaultFilter;
|
||||
|
||||
// Allow some checks to be injected
|
||||
if (options.isHostNewlyUp) {
|
||||
this._isHostNewlyUp = options.isHostNewlyUp;
|
||||
}
|
||||
if (options.healthCheck) {
|
||||
this._healthCheck = options.healthCheck;
|
||||
}
|
||||
if (options.compare) {
|
||||
this._compare = options.compare;
|
||||
}
|
||||
if (options.getReplicas) {
|
||||
this._getReplicas = options.getReplicas;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the load balancing policy, called after the driver obtained the information of the cluster.
|
||||
* @param {Client} client
|
||||
* @param {HostMap} hosts
|
||||
* @param {Function} callback
|
||||
*/
|
||||
init(client, hosts, callback) {
|
||||
this._client = client;
|
||||
this._hosts = hosts;
|
||||
|
||||
// Clean local host cache
|
||||
this._hosts.on('add', () => this._filteredHosts = null);
|
||||
this._hosts.on('remove', () => this._filteredHosts = null);
|
||||
|
||||
try {
|
||||
setLocalDc(this, client, this._hosts);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the distance assigned by this policy to the provided host, relatively to the client instance.
|
||||
* @param {Host} host
|
||||
*/
|
||||
getDistance(host) {
|
||||
if (this._preferredHost !== null && host === this._preferredHost) {
|
||||
// Set the last preferred host as local.
|
||||
// It ensures that the pool for the graph analytics host has the appropriate size
|
||||
return types.distance.local;
|
||||
}
|
||||
|
||||
if (!this._filter(host)) {
|
||||
return types.distance.ignored;
|
||||
}
|
||||
|
||||
return host.datacenter === this.localDc ? types.distance.local : types.distance.ignored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a host iterator to be used for a query execution.
|
||||
* @override
|
||||
* @param {String} keyspace
|
||||
* @param {ExecutionOptions} executionOptions
|
||||
* @param {Function} callback
|
||||
*/
|
||||
newQueryPlan(keyspace, executionOptions, callback) {
|
||||
let routingKey;
|
||||
let preferredHost;
|
||||
|
||||
if (executionOptions) {
|
||||
routingKey = executionOptions.getRoutingKey();
|
||||
|
||||
if (executionOptions.getKeyspace()) {
|
||||
keyspace = executionOptions.getKeyspace();
|
||||
}
|
||||
|
||||
preferredHost = executionOptions.getPreferredHost();
|
||||
}
|
||||
|
||||
let iterable;
|
||||
|
||||
if (!keyspace || !routingKey) {
|
||||
iterable = this._getLocalHosts();
|
||||
} else {
|
||||
iterable = this._getReplicasAndLocalHosts(keyspace, routingKey);
|
||||
}
|
||||
|
||||
if (preferredHost) {
|
||||
// Set it on an instance level field to set the distance
|
||||
this._preferredHost = preferredHost;
|
||||
iterable = DefaultLoadBalancingPolicy._getPreferredHostFirst(preferredHost, iterable);
|
||||
}
|
||||
|
||||
return callback(null, iterable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields the preferred host first, followed by the host in the provided iterable
|
||||
* @param preferredHost
|
||||
* @param iterable
|
||||
* @private
|
||||
*/
|
||||
static *_getPreferredHostFirst(preferredHost, iterable) {
|
||||
yield preferredHost;
|
||||
|
||||
for (const host of iterable) {
|
||||
if (host !== preferredHost) {
|
||||
yield host;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields the local hosts without the replicas already yielded
|
||||
* @param {Array<Host>} [localReplicas] The local replicas that we should avoid to include again
|
||||
* @private
|
||||
*/
|
||||
*_getLocalHosts(localReplicas) {
|
||||
// Use a local reference
|
||||
const hosts = this._getFilteredLocalHosts();
|
||||
const initialIndex = this._getIndex();
|
||||
|
||||
// indexOf() over an Array is a O(n) operation but given that there should be 3 to 7 replicas,
|
||||
// it shouldn't be an expensive call. Additionally, this will only be executed when the local replicas
|
||||
// have been exhausted in a lazy manner.
|
||||
const canBeYield = localReplicas
|
||||
? h => localReplicas.indexOf(h) === -1
|
||||
: h => true;
|
||||
|
||||
for (let i = 0; i < hosts.length; i++) {
|
||||
const h = hosts[(i + initialIndex) % hosts.length];
|
||||
if (canBeYield(h) && h.isUp()) {
|
||||
yield h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_getReplicasAndLocalHosts(keyspace, routingKey) {
|
||||
let replicas = this._getReplicas(keyspace, routingKey);
|
||||
if (replicas === null) {
|
||||
return this._getLocalHosts();
|
||||
}
|
||||
|
||||
const filteredReplicas = [];
|
||||
let newlyUpReplica = null;
|
||||
let newlyUpReplicaTimestamp = Number.MIN_SAFE_INTEGER;
|
||||
let unhealthyReplicas = 0;
|
||||
|
||||
// Filter by DC, predicate and UP replicas
|
||||
// Use the same iteration to perform other checks: whether if its newly UP or unhealthy
|
||||
// As this is part of the hot path, we use a simple loop and avoid using Array.prototype.filter() + closure
|
||||
for (let i = 0; i < replicas.length; i++) {
|
||||
const h = replicas[i];
|
||||
if (!this._filter(h) || h.datacenter !== this.localDc || !h.isUp()) {
|
||||
continue;
|
||||
}
|
||||
const isUpSince = this._isHostNewlyUp(h);
|
||||
if (isUpSince !== null && isUpSince > newlyUpReplicaTimestamp) {
|
||||
newlyUpReplica = h;
|
||||
newlyUpReplicaTimestamp = isUpSince;
|
||||
}
|
||||
if (newlyUpReplica === null && !this._healthCheck(h)) {
|
||||
unhealthyReplicas++;
|
||||
}
|
||||
filteredReplicas.push(h);
|
||||
}
|
||||
|
||||
replicas = filteredReplicas;
|
||||
|
||||
// Shuffle remaining local replicas
|
||||
utils.shuffleArray(replicas);
|
||||
|
||||
if (replicas.length < 3) {
|
||||
// Avoid reordering replicas of a set of 2 as we could be doing more harm than good
|
||||
return this.yieldReplicasFirst(replicas);
|
||||
}
|
||||
|
||||
let temp;
|
||||
|
||||
if (newlyUpReplica === null) {
|
||||
if (unhealthyReplicas > 0 && unhealthyReplicas < Math.floor(replicas.length / 2 + 1)) {
|
||||
// There is one or more unhealthy replicas and there is a majority of healthy replicas
|
||||
this._sendUnhealthyToTheBack(replicas, unhealthyReplicas);
|
||||
}
|
||||
}
|
||||
else if ((newlyUpReplica === replicas[0] || newlyUpReplica === replicas[1]) && Math.random() * 4 >= 1) {
|
||||
// There is a newly UP replica and the replica in first or second position is the most recent replica
|
||||
// marked as UP and dice roll 1d4!=1 -> Send it to the back of the Array
|
||||
const index = newlyUpReplica === replicas[0] ? 0 : 1;
|
||||
temp = replicas[replicas.length - 1];
|
||||
replicas[replicas.length - 1] = replicas[index];
|
||||
replicas[index] = temp;
|
||||
}
|
||||
|
||||
if (this._compare(replicas[1], replicas[0]) > 0) {
|
||||
// Power of two random choices
|
||||
temp = replicas[0];
|
||||
replicas[0] = replicas[1];
|
||||
replicas[1] = temp;
|
||||
}
|
||||
|
||||
return this.yieldReplicasFirst(replicas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields the local replicas followed by the rest of local nodes.
|
||||
* @param {Array<Host>} replicas The local replicas
|
||||
*/
|
||||
*yieldReplicasFirst(replicas) {
|
||||
for (let i = 0; i < replicas.length; i++) {
|
||||
yield replicas[i];
|
||||
}
|
||||
yield* this._getLocalHosts(replicas);
|
||||
}
|
||||
|
||||
_isHostNewlyUp(h) {
|
||||
return (h.isUpSince !== null && Date.now() - h.isUpSince < newlyUpInterval) ? h.isUpSince : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a boolean determining whether the host health is ok or not.
|
||||
* A Host is considered unhealthy when there are enough items in the queue (10 items in-flight) but the
|
||||
* Host is not responding to those requests.
|
||||
* @param {Host} h
|
||||
* @return {boolean}
|
||||
* @private
|
||||
*/
|
||||
_healthCheck(h) {
|
||||
return !(h.getInFlight() >= 10 && h.getResponseCount() <= 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares to host and returns 1 if it needs to favor the first host otherwise, -1.
|
||||
* @return {number}
|
||||
* @private
|
||||
*/
|
||||
_compare(h1, h2) {
|
||||
return h1.getInFlight() < h2.getInFlight() ? 1 : -1;
|
||||
}
|
||||
|
||||
_getReplicas(keyspace, routingKey) {
|
||||
return this._client.getReplicas(keyspace, routingKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Array of hosts filtered by DC and predicate.
|
||||
* @returns {Array<Host>}
|
||||
* @private
|
||||
*/
|
||||
_getFilteredLocalHosts() {
|
||||
if (this._filteredHosts === null) {
|
||||
this._filteredHosts = this._hosts.values()
|
||||
.filter(h => this._filter(h) && h.datacenter === this.localDc);
|
||||
}
|
||||
return this._filteredHosts;
|
||||
}
|
||||
|
||||
_getIndex() {
|
||||
const result = this._index++;
|
||||
// Overflow protection
|
||||
if (this._index === 0x7fffffff) {
|
||||
this._index = 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
_sendUnhealthyToTheBack(replicas, unhealthyReplicas) {
|
||||
let counter = 0;
|
||||
|
||||
// Start from the back, move backwards and stop once all unhealthy replicas are at the back
|
||||
for (let i = replicas.length - 1; i >= 0 && counter < unhealthyReplicas; i--) {
|
||||
const host = replicas[i];
|
||||
if (this._healthCheck(host)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetIndex = replicas.length - 1 - counter;
|
||||
if (targetIndex !== i) {
|
||||
const temp = replicas[targetIndex];
|
||||
replicas[targetIndex] = host;
|
||||
replicas[i] = temp;
|
||||
}
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
_defaultFilter() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an associative array containing the policy options.
|
||||
*/
|
||||
getOptions() {
|
||||
return new Map([
|
||||
['localDataCenter', this.localDc ],
|
||||
['filterFunction', this._filter !== this._defaultFilter ]
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and sets the local data center to be used.
|
||||
* @param {LoadBalancingPolicy} lbp
|
||||
* @param {Client} client
|
||||
* @param {HostMap} hosts
|
||||
* @private
|
||||
*/
|
||||
function setLocalDc(lbp, client, hosts) {
|
||||
if (!(lbp instanceof LoadBalancingPolicy)) {
|
||||
throw new errors.DriverInternalError('LoadBalancingPolicy instance was not provided');
|
||||
}
|
||||
|
||||
if (client && client.options) {
|
||||
if (lbp.localDc && !client.options.localDataCenter) {
|
||||
client.log('info', `Local data center '${lbp.localDc}' was provided as an argument to the load-balancing` +
|
||||
` policy. It is preferable to specify the local data center using 'localDataCenter' in Client` +
|
||||
` options instead when your application is targeting a single data center.`);
|
||||
}
|
||||
|
||||
// If localDc is unset, use value set in client options.
|
||||
lbp.localDc = lbp.localDc || client.options.localDataCenter;
|
||||
}
|
||||
|
||||
const dcs = getDataCenters(hosts);
|
||||
|
||||
if (!lbp.localDc) {
|
||||
throw new errors.ArgumentError(
|
||||
`'localDataCenter' is not defined in Client options and also was not specified in constructor.` +
|
||||
` At least one is required. Available DCs are: [${Array.from(dcs)}]`);
|
||||
}
|
||||
|
||||
if (!dcs.has(lbp.localDc)) {
|
||||
throw new errors.ArgumentError(`Datacenter ${lbp.localDc} was not found. Available DCs are: [${Array.from(dcs)}]`);
|
||||
}
|
||||
}
|
||||
|
||||
function getDataCenters(hosts) {
|
||||
return new Set(hosts.values().map(h => h.datacenter));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AllowListPolicy,
|
||||
DCAwareRoundRobinPolicy,
|
||||
DefaultLoadBalancingPolicy,
|
||||
LoadBalancingPolicy,
|
||||
RoundRobinPolicy,
|
||||
TokenAwarePolicy,
|
||||
// Deprecated: for backward compatibility only.
|
||||
WhiteListPolicy
|
||||
};
|
Reference in New Issue
Block a user