All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.dataforte.cassandra.pool.PooledConnection Maven / Gradle / Ivy

There is a newer version: 1.0.0.CR1
Show newest version
/**
 * 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 attributes = new HashMap();
    
    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 hosts = parent.getCassandraRing().getHosts();
        Iterator 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 getAttributes() {
        return attributes;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy