/**
* Copyright 2010 Tristan Tarrant
*
* 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 net.dataforte.cassandra.pool;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.apache.cassandra.thrift.AuthenticationException;
import org.apache.cassandra.thrift.AuthenticationRequest;
import org.apache.cassandra.thrift.AuthorizationException;
import org.apache.cassandra.thrift.Cassandra;
import org.apache.cassandra.thrift.InvalidRequestException;
import org.apache.thrift.TException;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TFastFramedTransport;
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;
/**
* Represents a pooled connection
* and holds a reference to the {@link org.apache.cassandra.thrift.Cassandra.Client} and {@link org.apache.thrift.transport.TTransport} object
*
* Derived from org.apache.tomcat.jdbc.pool.PooledConnection by fhanik
*
* @version 1.0
*/
public class PooledConnection {
/**
* Logger
*/
private static final Logger log = LoggerFactory.getLogger(PooledConnection.class);
/**
* Instance counter
*/
protected static AtomicInteger counter = new AtomicInteger(01);
/**
* Validate when connection is borrowed flag
*/
public static final int VALIDATE_BORROW = 1;
/**
* Validate when connection is returned flag
*/
public static final int VALIDATE_RETURN = 2;
/**
* Validate when connection is idle flag
*/
public static final int VALIDATE_IDLE = 3;
/**
* Validate when connection is initialized flag
*/
public static final int VALIDATE_INIT = 4;
/**
* The properties for the connection pool
*/
protected PoolConfiguration poolProperties;
/**
* The underlying database connection
*/
private volatile Cassandra.Client connection;
/**
* The underlying transport for the connection
*/
private volatile TTransport transport;
/**
* When we track abandon traces, this string holds the thread dump
*/
private String abandonTrace = null;
/**
* Timestamp the connection was last 'touched' by the pool
*/
private volatile long timestamp;
/**
* Lock for this connection only
*/
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
/**
* Set to true if this connection has been discarded by the pool
*/
private volatile boolean discarded = false;
/**
* The Timestamp when the last time the connect() method was called successfully
*/
private volatile long lastConnected = -1;
/**
* timestamp to keep track of validation intervals
*/
private volatile long lastValidated = System.currentTimeMillis();
/**
* The instance number for this connection
*/
private int instanceCount = 0;
/**
* The parent
*/
protected ConnectionPool parent;
private HashMap<Object, Object> attributes = new HashMap<Object, Object>();
private AtomicBoolean released = new AtomicBoolean(false);
private volatile boolean suspect = false;
/**
* Constructor
* @param prop - pool properties
* @param parent - the parent connection pool
*/
public PooledConnection(PoolConfiguration prop, ConnectionPool parent) {
instanceCount = counter.addAndGet(1);
poolProperties = prop;
this.parent = parent;
}
public void connect() throws TException {
if (released.get()) throw new TException("[" + parent.getName() + "] A connection once released, can't be reestablished.");
if (connection != null) {
try {
this.disconnect(false);
} catch (Exception x) {
log.debug("[" + parent.getName() + "] Unable to disconnect previous connection.", x);
} //catch
} //end if
List<CassandraHost> hosts = parent.getCassandraRing().getHosts();
Iterator<CassandraHost> hostIterator = hosts.iterator();
int tried = 0;
CassandraHost host = null;
for(this.transport=null; this.transport==null; ) {
if(tried>poolProperties.getFailoverPolicy().numRetries || !hostIterator.hasNext()) {
throw new TException("[" + parent.getName() + "] Could not connect to any hosts");
}
host = hostIterator.next();
// If the host is good or the validation interval has passed since last checking with it, attempt to get a connection
if(host.isGood() || (host.getLastUsed()+poolProperties.getHostRetryInterval() < System.currentTimeMillis())) {
try {
TSocket socket = new TSocket(host.getHost(), poolProperties.getPort(), poolProperties.getSocketTimeout());
if (poolProperties.isFramed())
this.transport = new TFastFramedTransport(socket);
else
this.transport = socket;
host.timestamp();
this.transport.open();
host.setGood(true);
} catch (TTransportException tte) {
host.timestamp();
host.setGood(false);
log.warn("[" + parent.getName() + "] Failed connection to "+host);
this.transport = null;
tried++;
}
}
}
TProtocol protocol = new TBinaryProtocol(this.transport);
this.connection = new Cassandra.Client(protocol);
if(poolProperties.getUsername()!=null) {
AuthenticationRequest authenticationRequest = new AuthenticationRequest();
authenticationRequest.putToCredentials("username", poolProperties.getUsername());
authenticationRequest.putToCredentials("password", poolProperties.getPassword());
try {
this.connection.login(authenticationRequest);
} catch (AuthenticationException e) {
this.disconnect(false);
throw new TException(e);
} catch (AuthorizationException e) {
this.disconnect(false);
throw new TException(e);
}
}
if(poolProperties.getKeySpace()!=null) {
try {
this.connection.set_keyspace(poolProperties.getKeySpace());
} catch (InvalidRequestException e) {
this.disconnect(false);
throw new TException(e);
}
}
this.discarded = false;
this.lastConnected = System.currentTimeMillis();
if(log.isDebugEnabled()) {
log.debug("[" + parent.getName() + "] Obtained a new connection to "+host);
}
}
/**
*
* @return true if connect() was called successfully and disconnect has not yet been called
*/
public boolean isInitialized() {
return connection!=null;
}
/**
* Issues a call to {@link #disconnect(boolean)} with the argument false followed by a call to
* {@link #connect()}
* @throws TException if the call to {@link #connect()} fails.
* @throws InvalidRequestException
* @throws AuthorizationException
* @throws AuthenticationException
*/
public void reconnect() throws TException, AuthenticationException, AuthorizationException, InvalidRequestException {
this.disconnect(false);
this.connect();
} //reconnect
/**
* Disconnects the connection. All exceptions are logged using debug level.
* @param finalize if set to true, a call to {@link ConnectionPool#finalize(PooledConnection)} is called.
*/
private void disconnect(boolean finalize) {
if (isDiscarded()) {
return;
}
setDiscarded(true);
if (connection != null) {
try {
parent.disconnectEvent(this, finalize);
transport.close();
}catch (Exception ignore) {
if (log.isDebugEnabled()) {
log.debug("[" + parent.getName() + "] Unable to close underlying Thrift connection", ignore);
}
}
}
connection = null;
transport = null;
lastConnected = -1;
if (finalize) parent.finalize(this);
}
//============================================================================
//
//============================================================================
/**
* Returns abandon timeout in milliseconds
* @return abandon timeout in milliseconds
*/
public long getAbandonTimeout() {
if (poolProperties.getRemoveAbandonedTimeout() <= 0) {
return Long.MAX_VALUE;
} else {
return poolProperties.getRemoveAbandonedTimeout()*1000;
} //end if
}
/**
* Returns true if the connection pool is configured
* to do validation for a certain action.
* @param action
* @return
*/
private boolean doValidate(int action) {
if (action == PooledConnection.VALIDATE_BORROW &&
poolProperties.isTestOnBorrow())
return true;
else if (action == PooledConnection.VALIDATE_RETURN &&
poolProperties.isTestOnReturn())
return true;
else if (action == PooledConnection.VALIDATE_IDLE &&
poolProperties.isTestWhileIdle())
return true;
else if (action == PooledConnection.VALIDATE_INIT &&
poolProperties.isTestOnConnect())
return true;
else
return false;
}
/**
* Returns true if the object is still valid. if not
* the pool will call the getExpiredAction() and follow up with one
* of the four expired methods
*/
public boolean validate(int validateAction) {
if (this.isDiscarded()) {
return false;
}
if (!doValidate(validateAction)) {
//no validation required
return true;
}
//Don't bother validating if already have recently enough
long now = System.currentTimeMillis();
if (validateAction!=VALIDATE_INIT &&
poolProperties.getValidationInterval() > 0 &&
(now - this.lastValidated) <
poolProperties.getValidationInterval()) {
return true;
}
try {
if(parent.getPoolProperties().isAutomaticHostDiscovery()) {
parent.getCassandraRing().refresh(connection); // Bonus: we validate the connection and also get an updated list of hosts from Cassandra
} else {
String cluster_name = connection.describe_cluster_name();
if(log.isTraceEnabled()) {
log.trace("[" + parent.getName() + "] Validated connection "+this.toString()+", cluster name = "+cluster_name);
}
}
this.lastValidated = now;
return true;
} catch (Exception ignore) {
if (log.isDebugEnabled())
log.debug("[" + parent.getName() + "] Unable to validate object:",ignore);
}
return false;
} //validate
/**
* The time limit for how long the object
* can remain unused before it is released
* @return {@link PoolConfiguration#getMinEvictableIdleTimeMillis()}
*/
public long getReleaseTime() {
return this.poolProperties.getMinEvictableIdleTimeMillis();
}
/**
* This method is called if (Now - timeCheckedIn > getReleaseTime())
* This method disconnects the connection, logs an error in debug mode if it happens
* then sets the {@link #released} flag to false. Any attempts to connect this cached object again
* will fail per {@link #connect()}
* The connection pool uses the atomic return value to decrement the pool size counter.
* @return true if this is the first time this method has been called. false if this method has been called before.
*/
public boolean release() {
try {
disconnect(true);
} catch (Exception x) {
if (log.isDebugEnabled()) {
log.debug("[" + parent.getName() + "] Unable to close Thrift connection",x);
}
}
return released.compareAndSet(false, true);
}
/**
* The pool will set the stack trace when it is check out and
* checked in
* @param trace the stack trace for this connection
*/
public void setStackTrace(String trace) {
abandonTrace = trace;
}
/**
* Returns the stack trace from when this connection was borrowed. Can return null if no stack trace was set.
* @return the stack trace or null of no trace was set
*/
public String getStackTrace() {
return abandonTrace;
}
/**
* Sets a timestamp on this connection. A timestamp usually means that some operation
* performed successfully.
* @param timestamp the timestamp as defined by {@link System#currentTimeMillis()}
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
setSuspect(false);
}
public boolean isSuspect() {
return suspect;
}
public void setSuspect(boolean suspect) {
this.suspect = suspect;
}
/**
* An interceptor can call this method with the value true, and the connection will be closed when it is returned to the pool.
* @param discarded - only valid value is true
* @throws IllegalStateException if this method is called with the value false and the value true has already been set.
*/
public void setDiscarded(boolean discarded) {
if (this.discarded && !discarded) throw new IllegalStateException("[" + parent.getName() + "] Unable to change the state once the connection has been discarded");
this.discarded = discarded;
}
/**
* Set the timestamp the connection was last validated.
* This flag is used to keep track when we are using a {@link PoolConfiguration#setValidationInterval(long) validation-interval}.
* @param lastValidated a timestamp as defined by {@link System#currentTimeMillis()}
*/
public void setLastValidated(long lastValidated) {
this.lastValidated = lastValidated;
}
/**
* Sets the pool configuration for this connection and connection pool.
* Object is shared with the {@link ConnectionPool}
* @param poolProperties
*/
public void setPoolProperties(PoolConfiguration poolProperties) {
this.poolProperties = poolProperties;
}
/**
* Return the timestamps of last pool action. Timestamps are typically set when connections
* are borrowed from the pool. It is used to keep track of {@link PoolConfiguration#setRemoveAbandonedTimeout(int) abandon-timeouts}.
*
* @return the timestamp of the last pool action as defined by {@link System#currentTimeMillis()}
*/
public long getTimestamp() {
return timestamp;
}
/**
* Returns the discarded flag.
* @return the discarded flag. If the value is true,
* either {@link #disconnect(boolean)} has been called or it will be called when the connection is returned to the pool.
*/
public boolean isDiscarded() {
return discarded;
}
/**
* Returns the timestamp of the last successful validation query execution.
* @return the timestamp of the last successful validation query execution as defined by {@link System#currentTimeMillis()}
*/
public long getLastValidated() {
return lastValidated;
}
/**
* Returns the configuration for this connection and pool
* @return the configuration for this connection and pool
*/
public PoolConfiguration getPoolProperties() {
return poolProperties;
}
/**
* Locks the connection only if either {@link PoolConfiguration#isPoolSweeperEnabled()} or
* {@link PoolConfiguration#getUseLock()} return true. The per connection lock ensures thread safety is
* multiple threads are performing operations on the connection.
* Otherwise this is a noop for performance
*/
public void lock() {
if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
//optimized, only use a lock when there is concurrency
lock.writeLock().lock();
}
}
/**
* Unlocks the connection only if the sweeper is enabled
* Otherwise this is a noop for performance
*/
public void unlock() {
if (poolProperties.getUseLock() || this.poolProperties.isPoolSweeperEnabled()) {
//optimized, only use a lock when there is concurrency
lock.writeLock().unlock();
}
}
/**
* Returns the underlying connection
* @return the underlying Thrift connection
*/
public Cassandra.Client getConnection() {
return this.connection;
}
public TTransport getTransport() {
return this.transport;
}
/**
* Returns the timestamp of when the connection was last connected to the database. *
* @return the timestamp when this connection was created as defined by {@link System#currentTimeMillis()}
*/
public long getLastConnected() {
return lastConnected;
}
@Override
public String toString() {
return "PooledConnection[instance="+instanceCount+","+(connection!=null?connection.toString():"null")+"]";
}
/**
* Returns true if this connection has been released and wont be reused.
* @return true if the method {@link #release()} has been called
*/
public boolean isReleased() {
return released.get();
}
public HashMap<Object,Object> getAttributes() {
return attributes;
}
}