/*
* The MIT License
*
* Copyright (c) 2011 Dominic Williams, Daniel Washusen and contributors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.scale7.cassandra.pelops;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.cassandra.thrift.KsDef;
import org.apache.cassandra.thrift.TokenRange;
import org.scale7.portability.SystemProxy;
import org.slf4j.Logger;
/**
* A heavy thread safe object that maintains a list of nodes in the cluster. It's intended that
* one instance of the class be available in the JVM per cluster.
*
* <p><b>Note</b>: The timeout parameter on the various constructors refers to ALL thrift related operations.
* See: https://issues.apache.org/jira/browse/CASSANDRA-959</p>
*/
public class Cluster {
/**
* The default number of milliseconds to wait for an operation to complete.
*/
public static final int DEFAULT_TIMEOUT = 4000;
private final Logger logger = SystemProxy.getLoggerFromFactory(Cluster.class);
private String[] nodes;
private final IConnection.Config connectionConfig;
private final INodeFilter nodeFilter;
private boolean dynamicNodeDiscovery = false;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock lockRead = lock.readLock();
private final Lock lockWrite = lock.writeLock();
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT} with dynamic node discovery turned off.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
*/
public Cluster(String nodes, int thriftPort) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT), false);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT} with dynamic node discovery turned off.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,
null, sslTransportRequired, trustStorePath, trustStorePassword), false);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT} with dynamic node discovery turned off.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param connectionAuthenticator for node connection authentication
*/
public Cluster(String nodes, int thriftPort,IConnectionAuthenticator connectionAuthenticator) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,connectionAuthenticator), false);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT} with dynamic node discovery turned off.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param connectionAuthenticator for node connection authentication
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort,IConnectionAuthenticator connectionAuthenticator,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,connectionAuthenticator,
sslTransportRequired, trustStorePath, trustStorePassword), false);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT}.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
*/
public Cluster(String nodes, int thriftPort, boolean dynamicNodeDiscovery) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT), dynamicNodeDiscovery);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT}.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort, boolean dynamicNodeDiscovery,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,
sslTransportRequired, trustStorePath, trustStorePassword), dynamicNodeDiscovery);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT}.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param connectionAuthenticator for node connection authentication
*/
public Cluster(String nodes, int thriftPort, boolean dynamicNodeDiscovery,IConnectionAuthenticator connectionAuthenticator) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,connectionAuthenticator), dynamicNodeDiscovery);
}
/**
* Creates a new cluster using the {@link #DEFAULT_TIMEOUT}.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param connectionAuthenticator for node connection authentication
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort, boolean dynamicNodeDiscovery,IConnectionAuthenticator connectionAuthenticator,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, DEFAULT_TIMEOUT,connectionAuthenticator,
sslTransportRequired, trustStorePath, trustStorePassword), dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param timeout the number of milliseconds thrift should wait to complete an operation (zero or less disables the timeout)
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
*/
public Cluster(String nodes, int thriftPort, int timeout, boolean dynamicNodeDiscovery) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, timeout), dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param timeout the number of milliseconds thrift should wait to complete an operation (zero or less disables the timeout)
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort, int timeout, boolean dynamicNodeDiscovery,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, timeout,
sslTransportRequired, trustStorePath, trustStorePassword), dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param timeout the number of milliseconds thrift should wait to complete an operation (zero or less disables the timeout)
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param connectionAuthenticator for node connection authentication
*/
public Cluster(String nodes, int thriftPort, int timeout, boolean dynamicNodeDiscovery,IConnectionAuthenticator connectionAuthenticator) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, timeout,connectionAuthenticator), dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param thriftPort the thrift port
* @param timeout the number of milliseconds thrift should wait to complete an operation (zero or less disables the timeout)
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param connectionAuthenticator for node connection authentication
* @param sslTransportRequired is SSL transport required
* @param trustStorePath path to trust store
* @param trustStorePassword password to the trust store
*/
public Cluster(String nodes, int thriftPort, int timeout, boolean dynamicNodeDiscovery,IConnectionAuthenticator connectionAuthenticator,
boolean sslTransportRequired, String trustStorePath, String trustStorePassword) {
this(splitAndTrim(nodes), new IConnection.Config(thriftPort, true, timeout,connectionAuthenticator,
sslTransportRequired, trustStorePath, trustStorePassword), dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param connectionConfig the connection config
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
*/
public Cluster(String nodes, IConnection.Config connectionConfig, boolean dynamicNodeDiscovery) {
this(splitAndTrim(nodes), connectionConfig, dynamicNodeDiscovery);
}
/**
* Creates a new cluster.
* @param nodes array of nodes
* @param connectionConfig the connection config
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
*/
public Cluster(String[] nodes, IConnection.Config connectionConfig, boolean dynamicNodeDiscovery) {
this(nodes, connectionConfig, dynamicNodeDiscovery, new AcceptAllNodeFilter());
}
/**
* Creates a new cluster.
* @param nodes array of nodes
* @param connectionConfig the connection config
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param nodeFilter used to filter nodes when dynamic node discovery is enabled
*/
public Cluster(String[] nodes, IConnection.Config connectionConfig, boolean dynamicNodeDiscovery, INodeFilter nodeFilter) {
this.connectionConfig = connectionConfig;
this.nodeFilter = nodeFilter;
this.dynamicNodeDiscovery = dynamicNodeDiscovery;
// make sure there are no duplicates
this.nodes = new HashSet<String>(Arrays.asList(nodes)).toArray(new String[nodes.length]);
if (!dynamicNodeDiscovery) {
logger.info("Dynamic node discovery is disabled, using {} as a static list of nodes", Arrays.toString(nodes));
} else {
logger.info("Dynamic node discovery is enabled, detecting initial list of nodes from {}", Arrays.toString(nodes));
refresh();
}
}
/**
* Creates a new cluster.
* @param nodes comma separated list of nodes
* @param connectionConfig the connection config
* @param dynamicNodeDiscovery true if nodes should be discovered dynamically
* @param nodeFilter used to filter nodes when dynamic node discovery is enabled
*/
public Cluster(String nodes, IConnection.Config connectionConfig, boolean dynamicNodeDiscovery, INodeFilter nodeFilter) {
this(splitAndTrim(nodes), connectionConfig, dynamicNodeDiscovery, nodeFilter);
}
/**
* Splits the provided string based on "," and trims leading or trailing whitespace for each host.
* @param contactNodes the nodes
* @return the split and trimmed nodes
*/
private static String[] splitAndTrim(String contactNodes) {
String[] splitContactNodes = contactNodes.split(",");
for (int i = 0; i < splitContactNodes.length; i++) {
splitContactNodes[i] = splitContactNodes[i].trim();
}
return splitContactNodes;
}
/**
* Configuration used to open new connections.
* @return the connection config
*/
public IConnection.Config getConnectionConfig() {
return connectionConfig;
}
/**
* The current list of available nodes.
* <p><b>Note</b>: avoid calling this method is a tight loop.
* @return a copy of the current nodes
*/
public Node[] getNodes() {
try {
lockRead.lock();
Node[] nodes = new Node[this.nodes.length];
for (int i = 0; i < this.nodes.length; i++) {
String hostAddress = this.nodes[i];
nodes[i] = new Node(hostAddress, getConnectionConfig());
}
return nodes;
} finally {
lockRead.unlock();
}
}
/**
* Refresh the current list of nodes.
* @param keyspace optional keyspace name used to obtain the node ring
*/
public void refresh(String keyspace) {
if (!dynamicNodeDiscovery)
return;
String[] latestNodes;
try {
if (keyspace != null)
latestNodes = refreshInternal(keyspace);
else
latestNodes = refreshInternal();
} catch (Exception e) {
logger.error("Failed to discover nodes dynamically, using existing list of nodes. See cause for details...", e);
return;
}
try {
lockWrite.lock();
nodes = latestNodes;
} finally {
lockWrite.unlock();
}
}
/**
* Refresh the current list of nodes.
*/
public void refresh() {
refresh(null);
}
/**
* Refresh the snapshot of the list of nodes currently believed to exist in the Cassandra cluster.
* @return the list of nodes
*/
private String[] refreshInternal() throws Exception {
KeyspaceManager kspcMngr = new ClusterKeyspaceManager(this);
List<KsDef> keyspaces = kspcMngr.getKeyspaceNames();
Iterator<KsDef> k = keyspaces.iterator();
KsDef appKeyspace = null;
while (k.hasNext()) {
KsDef keyspace = k.next();
if (!keyspace.getName().equals("system")) {
appKeyspace = keyspace;
break;
}
}
if (appKeyspace == null)
throw new Exception("Cannot obtain a node list from a ring mapping. No keyspaces are defined for this cluster.");
return refreshInternal(appKeyspace.getName());
}
private String[] refreshInternal(String keyspace) throws Exception {
KeyspaceManager manager = Pelops.createKeyspaceManager(this);
logger.debug("Fetching nodes using keyspace '{}'", keyspace);
List<TokenRange> mappings = manager.getKeyspaceRingMappings(keyspace);
Set<String> clusterNodes = new HashSet<String>();
for (TokenRange tokenRange : mappings) {
List<String> endPointList = tokenRange.getEndpoints();
clusterNodes.addAll(endPointList);
}
Iterator<String> iterator = clusterNodes.iterator();
while (iterator.hasNext()) {
String node = iterator.next();
logger.debug("Checking node '{}' against node filter", node);
if (!nodeFilter.accept(node)) {
logger.debug("Removing node '{}' as directed by node filter", node);
iterator.remove();
}
}
String[] nodes = clusterNodes.toArray(new String[clusterNodes.size()]);
logger.debug("Final set of refreshed nodes: {}", Arrays.toString(nodes));
return nodes;
}
/**
* A filter used to determine which nodes should be used when {@link org.scale7.cassandra.pelops.Cluster#refresh()
* refreshing}. Implementations could potentially filter nodes that are in other data centers etc.
*/
public static interface INodeFilter {
boolean accept(String node);
}
/**
* Default implementation that accepts all nodes.
*/
public static class AcceptAllNodeFilter implements INodeFilter {
@Override
public boolean accept(String node) {
return true;
}
}
/**
* Represents a node in the cluster.
*/
public static class Node {
private final String address;
private final IConnection.Config config;
public Node(String address, IConnection.Config config) {
this.address = address;
this.config = config;
}
public String getAddress() {
return address;
}
public IConnection.Config getConfig() {
return config;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Node node = (Node) o;
if (!address.equals(node.address)) return false;
return true;
}
@Override
public int hashCode() {
return address.hashCode();
}
@Override
public String toString() {
return address + ":" + config.getThriftPort();
}
}
public class ClusterKeyspaceManager extends KeyspaceManager {
public ClusterKeyspaceManager(Cluster cluster) {
super(cluster, 0);
}
}
}