// Copyright 2017 JanusGraph Authors
//
// 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.
package org.janusgraph.diskstorage.cassandra.thrift.thriftpool;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.cassandra.auth.IAuthenticator;
import org.apache.cassandra.thrift.AuthenticationRequest;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.pool.KeyedPoolableObjectFactory;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSSLTransportFactory;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
import org.apache.thrift.transport.TTransportException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A factory compatible with Apache commons-pool for Cassandra Thrift API
* connections.
*
* @author Dan LaRocque <dalaro@hopcount.org>
*/
public class CTConnectionFactory implements KeyedPoolableObjectFactory<String, CTConnection> {
private static final Logger log = LoggerFactory.getLogger(CTConnectionFactory.class);
private static final long SCHEMA_WAIT_MAX = 5000L;
private static final long SCHEMA_WAIT_INCREMENT = 25L;
private final AtomicReference<Config> cfgRef;
private CTConnectionFactory(Config config) {
this.cfgRef = new AtomicReference<Config>(config);
}
@Override
public void activateObject(String key, CTConnection c) throws Exception {
// Do nothing, as in passivateObject
}
@Override
public void destroyObject(String key, CTConnection c) throws Exception {
TTransport t = c.getTransport();
if (t.isOpen()) {
t.close();
log.trace("Closed transport {}", t);
} else {
log.trace("Not closing transport {} (already closed)", t);
}
}
@Override
public CTConnection makeObject(String key) throws Exception {
CTConnection conn = makeRawConnection();
Cassandra.Client client = conn.getClient();
client.set_keyspace(key);
return conn;
}
/**
* Create a Cassandra-Thrift connection, but do not attempt to
* set a keyspace on the connection.
*
* @return A CTConnection ready to talk to a Cassandra cluster
* @throws TTransportException on any Thrift transport failure
*/
public CTConnection makeRawConnection() throws TTransportException {
final Config cfg = cfgRef.get();
String hostname = cfg.getRandomHost();
log.debug("Creating TSocket({}, {}, {}, {}, {})", hostname, cfg.port, cfg.username, cfg.password, cfg.timeoutMS);
TSocket socket;
if (null != cfg.sslTruststoreLocation && !cfg.sslTruststoreLocation.isEmpty()) {
TSSLTransportFactory.TSSLTransportParameters params = new TSSLTransportFactory.TSSLTransportParameters() {{
setTrustStore(cfg.sslTruststoreLocation, cfg.sslTruststorePassword);
}};
socket = TSSLTransportFactory.getClientSocket(hostname, cfg.port, cfg.timeoutMS, params);
} else {
socket = new TSocket(hostname, cfg.port, cfg.timeoutMS);
}
TTransport transport = new TFramedTransport(socket, cfg.frameSize);
log.trace("Created transport {}", transport);
TBinaryProtocol protocol = new TBinaryProtocol(transport);
Cassandra.Client client = new Cassandra.Client(protocol);
if (!transport.isOpen()) {
transport.open();
}
if (cfg.username != null) {
Map<String, String> credentials = new HashMap<String, String>() {{
put(IAuthenticator.USERNAME_KEY, cfg.username);
put(IAuthenticator.PASSWORD_KEY, cfg.password);
}};
try {
client.login(new AuthenticationRequest(credentials));
} catch (Exception e) { // TTransportException will propagate authentication/authorization failure
throw new TTransportException(e);
}
}
return new CTConnection(transport, client, cfg);
}
@Override
public void passivateObject(String key, CTConnection o) throws Exception {
// Do nothing, as in activateObject
}
@Override
public boolean validateObject(String key, CTConnection c) {
Config curCfg = cfgRef.get();
boolean isSameConfig = c.getConfig().equals(curCfg);
if (log.isDebugEnabled()) {
if (isSameConfig) {
log.trace("Validated {} by configuration {}", c, curCfg);
} else {
log.trace("Rejected {}; current config is {}; rejected connection config is {}",
c, curCfg, c.getConfig());
}
}
boolean isOpen = c.isOpen();
try {
c.getClient().describe_version();
} catch (TException e) {
isOpen = false;
}
return isSameConfig && isOpen;
}
public static class Config {
private final String[] hostnames;
private final int port;
private final String username;
private final String password;
private final Random random;
private int timeoutMS;
private int frameSize;
private String sslTruststoreLocation;
private String sslTruststorePassword;
private boolean isBuilt;
public Config(String[] hostnames, int port, String username, String password) {
this.hostnames = hostnames;
this.port = port;
this.username = username;
this.password = password;
this.random = new Random();
}
// TODO: we don't really need getters/setters here as all of the fields are final and immutable
public String getHostname() {
return hostnames[0];
}
public int getPort() {
return port;
}
public String getRandomHost() {
return hostnames.length == 1 ? hostnames[0] : hostnames[random.nextInt(hostnames.length)];
}
public Config setTimeoutMS(int timeoutMS) {
checkIfAlreadyBuilt();
this.timeoutMS = timeoutMS;
return this;
}
public Config setFrameSize(int frameSize) {
checkIfAlreadyBuilt();
this.frameSize = frameSize;
return this;
}
public Config setSSLTruststoreLocation(String location) {
checkIfAlreadyBuilt();
this.sslTruststoreLocation = location;
return this;
}
public Config setSSLTruststorePassword(String password) {
checkIfAlreadyBuilt();
this.sslTruststorePassword = password;
return this;
}
public CTConnectionFactory build() {
isBuilt = true;
return new CTConnectionFactory(this);
}
public void checkIfAlreadyBuilt() {
if (isBuilt)
throw new IllegalStateException("Can't accept modifications when used with built factory.");
}
@Override
public String toString() {
return "Config[hostnames=" + StringUtils.join(hostnames, ',') + ", port=" + port
+ ", timeoutMS=" + timeoutMS + ", frameSize=" + frameSize
+ "]";
}
}
}