package org.apache.hadoop.hive.cassandra;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.CfDef;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.cassandra.thrift.KsDef;
import org.apache.cassandra.thrift.SchemaDisagreementException;
import org.apache.cassandra.thrift.TimedOutException;
import org.apache.cassandra.thrift.TokenRange;
import org.apache.cassandra.thrift.UnavailableException;
import org.apache.log4j.Logger;
import org.apache.thrift.TException;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
/**
* A proxy client connects to cassandra backend server.
*
*/
public class CassandraProxyClient implements java.lang.reflect.InvocationHandler {
private static final Logger logger = Logger.getLogger(CassandraProxyClient.class);
/**
* The initial host to create the proxy client.
*/
private final String host;
private final int port;
/**
* The last successfully connected server.
*/
private String lastUsedHost;
/**
* Last time the ring was checked.
*/
private long lastPoolCheck;
/**
* Cassandra thrift client.
*/
private CassandraClientHolder clientHolder;
/**
* The key space to get the ring information from.
*/
private String ringKs;
/**
* Option to choose the next server from the ring. Default is RoundRobin unless
* specified by the client to choose randomizer.
*/
private RingConnOption nextServerGen;
/**
* Maximum number of attempts when connection is lost.
*/
private final int maxAttempts = 10;
/**
* Construct a proxy connection.
*
* @param host
* cassandra host
* @param port
* cassandra port
* @param framed
* true to used framed connection
* @param randomizeConnections
* true if randomly choosing a server when connection fails; false to use round-robin
* mechanism
* @return a Brisk Client Interface
* @throws IOException
*/
public CassandraProxyClient(String host, int port, boolean framed, boolean randomizeConnections)
throws CassandraException {
this.host = host;
this.port = port;
this.lastUsedHost = host;
this.lastPoolCheck = 0;
// If randomized to choose a connection, initialize the random generator.
if (randomizeConnections) {
nextServerGen = new RandomizerOption();
} else {
nextServerGen = new RoundRobinOption();
}
initializeConnection();
}
/**
* Return a handle to the proxied connection
* @return
*/
public Cassandra.Iface getProxyConnection() {
return (Cassandra.Iface) java.lang.reflect.Proxy.newProxyInstance(Cassandra.Client.class
.getClassLoader(),Cassandra.Client.class.getInterfaces(), this);
}
/**
* Delegates to close of {@link CassandraClientHolder#close()}
*/
public void close() {
if (clientHolder != null)
{
clientHolder.close();
}
}
/**
* Create connection to a given host.
*
* @param host
* cassandra host
* @return cassandra thrift client
* @throws CassandraException
* error
*/
private CassandraClientHolder createConnection(String host) throws CassandraException {
TSocket socket = new TSocket(host, port);
TTransport trans = new TFramedTransport(socket);
CassandraClientHolder ch = new CassandraClientHolder(trans);
return ch;
}
/**
* Initialize the cassandra connection with the initial given cassandra host.
* Create a temporary keyspace if no one exists. Otherwise, choose the first non-system
* keyspace to describe the ring.
* Initialize the ring.
*
* @throws IOException
*/
private void initializeConnection() throws CassandraException {
clientHolder = createConnection(host);
if (logger.isDebugEnabled()) {
logger.debug("Connected to cassandra at " + host + ":" + port);
}
assert clientHolder.isOpen();
// Find the first keyspace that's not system and assign it to the lastly used keyspace.
try {
List<KsDef> allKs = clientHolder.getClient().describe_keyspaces();
if (allKs.isEmpty() || (allKs.size() == 1 && allKs.get(0).name.equalsIgnoreCase("system"))) {
allKs.add(createTmpKs());
}
for (KsDef ks : allKs) {
if (!ks.name.equalsIgnoreCase("system")) {
ringKs = ks.name;
break;
}
}
// Set the ring keyspace for initialization purpose. This value
// should be overwritten later by set_keyspace
clientHolder.setKeyspace(ringKs);
} catch (InvalidRequestException e) {
throw new CassandraException(e);
} catch (TException e) {
throw new CassandraException(e);
} catch (SchemaDisagreementException e){
throw new CassandraException(e);
}
checkRing();
}
/**
* Create a temporary keyspace. This will only be called when there is no keyspace except system
* defined on (new cluster).
* However we need a keyspace to call describe_ring to get all servers from the ring.
*
* @return the temporary keyspace
* @throws InvalidRequestException
* error
* @throws TException
* error
* @throws SchemaDisagreementException
* @throws InterruptedException
* error
*/
private KsDef createTmpKs() throws InvalidRequestException, TException, SchemaDisagreementException {
Map<String, String> stratOpts = new HashMap<String, String>();
stratOpts.put("replication_factor", "1");
KsDef tmpKs = new KsDef("proxy_client_ks", "org.apache.cassandra.locator.SimpleStrategy",
Arrays.asList(new CfDef[] {})).setStrategy_options(stratOpts);
clientHolder.getClient().system_add_keyspace(tmpKs);
return tmpKs;
}
/**
* Refresh the server in the ring.
*
* @throws TException
* @throws InvalidRequestException
*
* @throws IOException
*/
private void checkRing() throws CassandraException {
assert clientHolder != null;
long now = System.currentTimeMillis();
if ((now - lastPoolCheck) > 60 * 1000) {
List<TokenRange> ring;
try {
ring = clientHolder.getClient().describe_ring(ringKs);
} catch (InvalidRequestException e) {
throw new CassandraException(e);
} catch (TException e) {
throw new CassandraException(e);
}
lastPoolCheck = now;
nextServerGen.resetRing(ring);
}
}
/**
* Attempt to connect to the next available server.
* If there is no server in the ring, an exception will be thrown out.
* If there is no server in the ring that is different from the server used last time,
* we should try the same server again in case that it recovers.
* Otherwise, try to connect to a different server.
*
* @throws error
* when there is no server to connect from the ring.
*/
private void attemptReconnect() throws CassandraException {
String endpoint = nextServerGen.getNextServer(lastUsedHost);
if (endpoint != null) {
clientHolder = createConnection(endpoint);
lastUsedHost = endpoint; // Assign the last successfully connected server.
checkRing(); // Refresh the servers in the ring.
logger.info("Connected to cassandra at " + endpoint + ":" + port);
} else {
clientHolder = createConnection(lastUsedHost);
}
}
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
Object result = null;
int tries = 0;
while (result == null && tries++ < maxAttempts) {
try {
if (clientHolder == null) {
// Let's try to connect to the next server.
attemptReconnect();
}
if (clientHolder != null && clientHolder.isOpen()) {
result = m.invoke(clientHolder.getClient(), args);
if (m.getName().equalsIgnoreCase("set_keyspace") && args != null && args.length == 1) {
// Keep last known keyspace when set_keyspace is successfully invoked.
ringKs = (String) args[0];
}
return result;
}
} catch (CassandraException e) {
// We are unable to connect to any server in the ring, let's continue trying
// until we hit the maximum number of attempts.
if (tries >= maxAttempts) {
throw e.getCause();
}
} catch (InvocationTargetException e) {
// Error is from cassandra thrift server
if (e.getTargetException() instanceof UnavailableException ||
e.getTargetException() instanceof TimedOutException ||
e.getTargetException() instanceof TTransportException) {
// These errors seem due to not being able to connect the cassandra server.
// If this is last try quit the program; otherwise keep trying.
if (tries >= maxAttempts) {
throw e.getCause();
}
} else {
// The other errors, we should not keep trying.
throw e.getCause();
}
}
}
throw new CassandraException("Not able to connect to any server in the ring " + lastUsedHost);
}
/**
* A class to implement the method of getting the next servers from the ring.
*
*/
public abstract class RingConnOption {
protected List<String> servers;
protected RingConnOption() {
}
protected RingConnOption(List<TokenRange> servers) {
this.servers = getAllServers(servers);
}
/**
* Return the next server from the ring. If there is no server in the ring, throw an exception.
* If there is only one server in the ring, return the server if it is different from the server
* tried last time.
* If there are more than two servers in the ring, return the server that is different from the
* server tried last time;
* if there is no server that is different from the server tried last time, return null;
*
* @param the
* last host used for connection
* @return next server for connection
*/
public String getNextServer(String host) throws CassandraException {
if (!checkServerHealth()) {
throw new CassandraException("No server is available from the ring.");
}
if (servers.size() == 1) {
if (servers.get(0).equals(host)) {
return null;
} else {
return servers.get(0);
}
} else {
return getServerFromRing(host);
}
}
/**
* Retrieve the next server from the ring.
*
* In the constructor, all servers from the ring are hashed and mapped. Theoretically there
* should be no duplicated server
* in the ring.
*
* @param host
* the last host used for connection
* @return new server for connection
*/
protected abstract String getServerFromRing(String host);
/**
* Reset the servers in the ring.
*/
public void resetRing(List<TokenRange> servers) {
this.servers = getAllServers(servers);
}
private List<String> getAllServers(List<TokenRange> input) {
HashMap<String, Integer> map = new HashMap<String, Integer>(input.size());
for (TokenRange thisRange : input) {
List<String> servers = thisRange.rpc_endpoints;
for (String newServer : servers) {
map.put(newServer, new Integer(1));
}
}
return new ArrayList<String>(map.keySet());
}
/**
* Check the health of the server ring.
*
* @return If there is no server in the ring, return false; Otherwise return true.
*/
private boolean checkServerHealth() {
if (servers == null || servers.size() == 0) {
logger.warn("No cassandra ring information found, no node is available to connect to");
return false;
}
return true;
}
}
/**
* Randomly choose a server from the ring to connect.
*/
public class RandomizerOption extends RingConnOption {
private final Random generator;
public RandomizerOption() {
super();
generator = new Random();
}
public RandomizerOption(List<TokenRange> rings) {
super(rings);
generator = new Random();
}
@Override
protected String getServerFromRing(String thisHost) {
String endpoint = thisHost;
while (!endpoint.equals(thisHost)) {
int index = generator.nextInt(servers.size());
endpoint = servers.get(index);
}
return endpoint;
}
}
/**
* Choose a server using round-robin mechanism.
*/
public class RoundRobinOption extends RingConnOption {
private int lastUsedIndex;
public RoundRobinOption() {
super();
}
public RoundRobinOption(List<TokenRange> rings) {
super(rings);
lastUsedIndex = 0;
}
@Override
protected String getServerFromRing(String thisHost) {
String endpoint = thisHost;
while (!endpoint.equals(thisHost)) {
lastUsedIndex++;
// Start from beginning if reaches to the last server in the ring.
if (lastUsedIndex == servers.size()) {
lastUsedIndex = 0;
}
endpoint = servers.get(lastUsedIndex);
}
return endpoint;
}
}
}