/**
* Alipay.com Inc.
* Copyright (c) 2004-2012 All Rights Reserved.
*/
package com.alipay.zdal.datasource.resource.connectionmanager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.security.auth.Subject;
import org.apache.log4j.Logger;
import com.alipay.zdal.common.Constants;
import com.alipay.zdal.datasource.client.util.ZConstants;
import com.alipay.zdal.datasource.resource.JBossResourceException;
import com.alipay.zdal.datasource.resource.ResourceException;
import com.alipay.zdal.datasource.resource.adapter.jdbc.local.LocalManagedConnectionFactory;
import com.alipay.zdal.datasource.resource.spi.ConnectionRequestInfo;
import com.alipay.zdal.datasource.resource.spi.ManagedConnection;
import com.alipay.zdal.datasource.resource.spi.ManagedConnectionFactory;
import com.alipay.zdal.datasource.resource.spi.ValidatingManagedConnectionFactory;
import com.alipay.zdal.datasource.resource.util.UnreachableStatementException;
/**
* The internal pool implementation
*
* @author ����
* @version $Id: InternalManagedConnectionPool.java, v 0.1 2014-1-6 ����05:34:47 Exp $
*/
public class InternalManagedConnectionPool {
private static final Logger logger = Logger
.getLogger(Constants.ZDAL_DATASOURCE_POOL_LOGNAME);
private static final String DEFAULT_FORMAT_STYLE = "yyyy-MM-dd HH:mm:ss";
private SimpleDateFormat sdf = new SimpleDateFormat(
DEFAULT_FORMAT_STYLE);
/** The managed connection factory */
private final ManagedConnectionFactory mcf;
/** The connection listener factory */
private final ConnectionListenerFactory clf;
/** The default subject */
private final Subject defaultSubject;
/** The default connection request information */
private final ConnectionRequestInfo defaultCri;
/** The pooling parameters */
private final PoolParams poolParams;
/** Copy of the maximum size from the pooling parameters.
* Dynamic changes to this value are not compatible with
* the semaphore which cannot change be dynamically changed.
*/
private final int maxSize;
/** The available connection event listeners */
private final ArrayList connectionListeners;
/** The permits used to control who can checkout a connection */
private final InternalSemaphore permits;
/** The log */
private final Logger log;
/** Whether trace is enabled */
private final boolean trace;
/** Stats */
private final Counter connectionCounter = new Counter();
/** The checked out connections */
private final HashSet checkedOut = new HashSet();
/** Whether the pool has been started */
private boolean started = false;
/** Whether the pool has been shutdown */
private final AtomicBoolean shutdown = new AtomicBoolean(false);
/** the max connections ever checked out **/
private volatile int maxUsedConnections = 0;
private String poolName;
/**
* Create a new internal pool
*
* @param mcf the managed connection factory
* @param subject the subject
* @param cri the connection request information
* @param poolParams the pooling parameters
* @param log the log
*/
protected InternalManagedConnectionPool(ManagedConnectionFactory mcf,
ConnectionListenerFactory clf, Subject subject,
ConnectionRequestInfo cri, PoolParams poolParams,
Logger log, String poolName) {
this.mcf = mcf;
this.clf = clf;
defaultSubject = subject;
defaultCri = cri;
this.poolParams = poolParams;
this.poolName = poolName;
this.maxSize = this.poolParams.maxSize;
this.log = log;
this.trace = log.isDebugEnabled();
connectionListeners = new ArrayList(this.maxSize);
permits = new InternalSemaphore(this.maxSize);
if (poolParams.prefill) {
PoolFiller.fillPool(this);
}
}
/**
* Initialize the pool
*/
protected void initialize() {
if (poolParams.idleTimeout != 0)
IdleRemover.registerPool(this, poolParams.idleTimeout);
if (poolParams.backgroundValidation) {
if (log.isDebugEnabled()) {
log.debug("Registering for background validation at interval "
+ poolParams.backgroundInterval);
}
ConnectionValidator.registerPool(this, poolParams.backgroundInterval);
}
//start a background thread for sofa checker, the interval is <blocking-timeout-millis>
}
public long getAvailableConnections() {
return permits.availablePermits();
}
public int getMaxConnectionsInUseCount() {
return maxUsedConnections;
}
public int getConnectionInUseCount() {
return checkedOut.size();
}
/**
* ǿ��ʹ���ӳص��ź�����ʵ�ʵ����ݿ������ӱ���һ��
*/
public void compareAndResetPermit() {
long total = getAvailableConnections() + getConnectionInUseCount();
int difference = (int) (maxSize - total);
if (difference > 0) {
permits.release(difference);
} else if (difference < 0) {
permits.reduceSemaphores(-difference);
}
}
/**
* todo distinguish between connection dying while match called
* and bad match strategy. In latter case we should put it back in
* the pool.
*/
public ConnectionListener getConnection(Subject subject, ConnectionRequestInfo cri)
throws ResourceException {
subject = (subject == null) ? defaultSubject : subject;
cri = (cri == null) ? defaultCri : cri;
long startWait = System.currentTimeMillis();
try {
if (permits.tryAcquire(poolParams.blockingTimeout, TimeUnit.MILLISECONDS)) {
//We have a permit to get a connection. Is there one in the pool already?
ConnectionListener cl = null;
do {
synchronized (connectionListeners) {
if (shutdown.get()) {
permits.release();
throw new ResourceException("The pool has been shutdown");
}
if (connectionListeners.size() > 0) {
cl = (ConnectionListener) connectionListeners
.remove(connectionListeners.size() - 1);
checkedOut.add(cl);
int size = (maxSize - permits.availablePermits());
//Update the maxUsedConnections
if (size > maxUsedConnections)
maxUsedConnections = size;
}
}
if (cl != null) {
//Yes, we retrieved a ManagedConnection from the pool. Does it match?
try {
Object matchedMC = mcf.matchManagedConnections(Collections.singleton(cl
.getManagedConnection()), subject, cri);
if (matchedMC != null) {
if (log.isDebugEnabled()) {
log.debug("supplying ManagedConnection from pool: " + cl);
}
cl.grantPermit(true);
return cl;
}
//Match did not succeed but no exception was thrown.
//Either we have the matching strategy wrong or the
//connection died while being checked. We need to
//distinguish these cases, but for now we always
//destroy the connection.
log
.warn("Destroying connection that could not be successfully matched: "
+ cl);
synchronized (connectionListeners) {
checkedOut.remove(cl);
}
doDestroy(cl, "getConnection");
cl = null;
} catch (Throwable t) {
log.warn(
"Throwable while trying to match ManagedConnection, destroying connection: "
+ cl, t);
synchronized (connectionListeners) {
checkedOut.remove(cl);
}
doDestroy(cl, "getConnection");
cl = null;
}
//We made it here, something went wrong and we should validate if we should continue attempting to acquire a connection
if (poolParams.useFastFail) {
if (log.isDebugEnabled()) {
log
.debug("Fast failing for connection attempt. No more attempts will be made to acquire connection from pool and a new connection will be created immeadiately");
}
break;
}
}
} while (connectionListeners.size() > 0);//end of do loop
//OK, we couldnt find a working connection from the pool. Make a new one.
try {
//No, the pool was empty, so we have to make a new one.
cl = createConnectionEventListener(subject, cri);
synchronized (connectionListeners) {
checkedOut.add(cl);
int size = (maxSize - permits.availablePermits());
if (size > maxUsedConnections)
maxUsedConnections = size;
}
//lack of synch on "started" probably ok, if 2 reads occur we will just
//run fillPool twice, no harm done.
if (started == false) {
started = true;
if (poolParams.minSize > 0)
PoolFiller.fillPool(this);
}
if (log.isDebugEnabled()) {
log.debug("supplying new ManagedConnection: " + cl);
}
cl.grantPermit(true);
return cl;
} catch (Throwable t) {
log.warn("Throwable while attempting to get a new connection: " + cl, t);
//return permit and rethrow
synchronized (connectionListeners) {
checkedOut.remove(cl);
}
permits.release();
JBossResourceException.rethrowAsResourceException(
"Unexpected throwable while trying to create a connection: " + cl, t);
throw new UnreachableStatementException();
}
} else {
if (this.maxSize == 0) {// ������������Ϊ0����˵��db���ڲ�����״̬
throw new ResourceException("��ǰ���ݿ�ڲ�����״̬,poolName = " + poolName,
ZConstants.ERROR_CODE_DB_NOT_AVAILABLE);
} else if (this.maxSize == this.maxUsedConnections) {
throw new ResourceException("����Դ��������������������ڳ�ʱʱ�䷶Χ��û���µ������ͷ�,poolName = "
+ poolName
+ " blocking timeout="
+ poolParams.blockingTimeout
+ "(ms),now-time = "
+ sdf.format(new Date())
+ ",connectionUrl = "
+ ((LocalManagedConnectionFactory) mcf)
.getConnectionURL(),
ZConstants.ERROR_CODE_CONNECTION_NOT_AVAILABLE);
} else {// ���ڳ�ʱ
throw new ResourceException(
"No ManagedConnections available within configured blocking timeout ( "
+ poolParams.blockingTimeout + " [ms] ),the poolName = " + poolName
+ ",connectionUrl = "
+ ((LocalManagedConnectionFactory) mcf).getConnectionURL(),
ZConstants.ERROR_CODE_CONNECTION_TIMEOUT);
}
}
} catch (InterruptedException ie) {
long end = System.currentTimeMillis() - startWait;
throw new ResourceException("Interrupted while requesting permit! Waited " + end
+ " ms", ie);
}
}
/**
*
* @param cl
* @param kill
*/
public void returnConnection(ConnectionListener cl, boolean kill) {
if (cl.getState() == ConnectionListener.DESTROYED) {
if (log.isDebugEnabled()) {
log.debug("ManagedConnection is being returned after it was destroyed" + cl);
}
if (cl.hasPermit()) {
// release semaphore
cl.grantPermit(false);
permits.release();
}
return;
}
if (trace)
if (log.isDebugEnabled()) {
log.debug("putting ManagedConnection back into pool kill=" + kill + " cl=" + cl);
}
try {
cl.getManagedConnection().cleanup();
} catch (ResourceException re) {
log.warn("ResourceException cleaning up ManagedConnection: " + cl, re);
kill = true;
}
// We need to destroy this one
if (cl.getState() == ConnectionListener.DESTROY)
kill = true;
synchronized (connectionListeners) {
checkedOut.remove(cl);
// This is really an error
if (kill == false && connectionListeners.size() >= poolParams.maxSize) {
log.warn("Destroying returned connection, maximum pool size exceeded " + cl);
kill = true;
}
// If we are destroying, check the connection is not in the pool
if (kill) {
// Adrian Brock: A resource adapter can asynchronously notify us that
// a connection error occurred.
// This could happen while the connection is not checked out.
// e.g. JMS can do this via an ExceptionListener on the connection.
// I have twice had to reinstate this line of code, PLEASE DO NOT REMOTE IT!
connectionListeners.remove(cl);
}
// return to the pool
else {
cl.used();
connectionListeners.add(cl);
}
if (cl.hasPermit()) {
// release semaphore
cl.grantPermit(false);
permits.release();
}
}
if (kill) {
if (trace)
if (log.isDebugEnabled()) {
log.debug("Destroying returned connection " + cl);
}
doDestroy(cl, "returnConnection");
}
}
/**
*
*/
public void flush() {
ArrayList destroy = null;
synchronized (connectionListeners) {
if (trace)
if (log.isDebugEnabled()) {
log.debug("Flushing pool checkedOut=" + checkedOut + " inPool="
+ connectionListeners);
}
// Mark checked out connections as requiring destruction
for (Iterator i = checkedOut.iterator(); i.hasNext();) {
ConnectionListener cl = (ConnectionListener) i.next();
if (trace)
if (log.isDebugEnabled()) {
log.debug("Flush marking checked out connection for destruction " + cl);
}
cl.setState(ConnectionListener.DESTROY);
}
// Destroy connections in the pool
while (connectionListeners.size() > 0) {
ConnectionListener cl = (ConnectionListener) connectionListeners.remove(0);
if (destroy == null)
destroy = new ArrayList();
destroy.add(cl);
}
}
// We need to destroy some connections
if (destroy != null) {
for (int i = 0; i < destroy.size(); ++i) {
ConnectionListener cl = (ConnectionListener) destroy.get(i);
if (trace)
if (log.isDebugEnabled()) {
log.debug("Destroying flushed connection " + cl);
}
doDestroy(cl, "flushConnectionPool");
}
// We destroyed something, check the minimum.
if (shutdown.get() == false && poolParams.minSize > 0)
PoolFiller.fillPool(this);
}
}
/**
*
*/
public void removeTimedOut() {
ArrayList destroy = null;
long timeout = System.currentTimeMillis() - poolParams.idleTimeout;
while (true) {
synchronized (connectionListeners) {
// Nothing left to destroy
if (connectionListeners.size() == 0)
break;
// Check the first in the list
ConnectionListener cl = (ConnectionListener) connectionListeners.get(0);
if (cl.isTimedOut(timeout)) {
// We need to destroy this one
connectionListeners.remove(0);
if (destroy == null)
destroy = new ArrayList();
destroy.add(cl);
} else {
//They were inserted chronologically, so if this one isn't timed out, following ones won't be either.
break;
}
}
}
// We found some connections to destroy
if (destroy != null) {
for (int i = 0; i < destroy.size(); ++i) {
ConnectionListener cl = (ConnectionListener) destroy.get(i);
if (trace) {
if (log.isDebugEnabled()) {
log.debug("Destroying timedout connection " + cl);
}
}
doDestroy(cl, "removeTimeout");
}
// We destroyed something, check the minimum.
if (shutdown.get() == false && poolParams.minSize > 0)
PoolFiller.fillPool(this);
}
}
/**
* For testing
*/
public void shutdownWithoutClear() {
IdleRemover.unregisterPool(this);
IdleRemover.waitForBackgroundThread();
ConnectionValidator.unRegisterPool(this);
ConnectionValidator.waitForBackgroundThread();
fillToMin();
shutdown.set(true);
}
/**
*
*/
public void shutdown() {
shutdown.set(true);
IdleRemover.unregisterPool(this);
ConnectionValidator.unRegisterPool(this);
flush();
}
/**
*
*/
public void fillToMin() {
while (true) {
// Get a permit - avoids a race when the pool is nearly full
// Also avoids unnessary fill checking when all connections are checked out
try {
if (permits.tryAcquire(poolParams.blockingTimeout, TimeUnit.MILLISECONDS)) {
try {
if (shutdown.get())
return;
// We already have enough connections
if (getMinSize() - connectionCounter.getGuaranteedCount() <= 0)
return;
// Create a connection to fill the pool
try {
ConnectionListener cl = createConnectionEventListener(defaultSubject,
defaultCri);
synchronized (connectionListeners) {
if (trace) {
if (log.isDebugEnabled()) {
log.debug("Filling pool cl=" + cl);
}
}
connectionListeners.add(cl);
}
} catch (ResourceException re) {
log.warn("Unable to fill pool ", re);
return;
}
} finally {
permits.release();
}
}
} catch (InterruptedException ignored) {
if (log.isDebugEnabled()) {
log.debug("Interrupted while requesting permit in fillToMin");
}
}
}
}
public int getConnectionCount() {
return connectionCounter.getCount();
}
public int getConnectionCreatedCount() {
return connectionCounter.getCreatedCount();
}
public int getConnectionDestroyedCount() {
return connectionCounter.getDestroyedCount();
}
/**
* Create a connection event listener
*
* @param subject the subject
* @param cri the connection request information
* @return the new listener
* @throws ResourceException for any error
*/
private ConnectionListener createConnectionEventListener(Subject subject,
ConnectionRequestInfo cri)
throws ResourceException {
ManagedConnection mc = mcf.createManagedConnection(subject, cri);
connectionCounter.inc();
try {
return clf.createConnectionListener(mc, this);
} catch (ResourceException re) {
connectionCounter.dec();
mc.destroy();
throw re;
}
}
/**
* Destroy a connection
*
* @param cl the connection to destroy
*/
public void doDestroy(ConnectionListener cl, String methodName) {
if (cl.getState() == ConnectionListener.DESTROYED) {
if (log.isDebugEnabled()) {
log.debug("ManagedConnection is already destroyed " + cl);
}
return;
}
connectionCounter.dec();
cl.setState(ConnectionListener.DESTROYED);
try {
cl.getManagedConnection().destroy();
logger.warn("WARN ## destroy a connection of poolName = " + poolName + " of "
+ methodName);
} catch (Throwable t) {
if (log.isDebugEnabled()) {
log.debug("Exception destroying ManagedConnection " + cl, t);
}
}
}
/**
*
* @throws Exception
*/
public void validateConnections() throws Exception {
if (trace)
if (log.isDebugEnabled()) {
log.debug("Attempting to validate connections for pool " + this);
}
if (permits.tryAcquire(poolParams.blockingTimeout, TimeUnit.MILLISECONDS)) {
boolean destroyed = false;
try {
while (true) {
ConnectionListener cl = null;
synchronized (connectionListeners) {
if (connectionListeners.size() == 0) {
break;
}
cl = removeForFrequencyCheck();
}
if (cl == null) {
break;
}
try {
Set candidateSet = Collections.singleton(cl.getManagedConnection());
if (mcf instanceof ValidatingManagedConnectionFactory) {
ValidatingManagedConnectionFactory vcf = (ValidatingManagedConnectionFactory) mcf;
candidateSet = vcf.getInvalidConnections(candidateSet);
if (candidateSet != null && candidateSet.size() > 0) {
if (cl.getState() != ConnectionListener.DESTROY) {
doDestroy(cl, "valiateConnection");
destroyed = true;
}
}
} else {
log
.warn("warning: background validation was specified with a non compliant ManagedConnectionFactory interface.");
}
} finally {
if (!destroyed) // FIXME: ֻҪ��һ����destroy����ô֮�������cl�����ᱻ������cls��
{
synchronized (connectionListeners) {
returnForFrequencyCheck(cl);
}
}
}
}
} finally {
permits.release();
if (destroyed && shutdown.get() == false && poolParams.minSize > 0) {
PoolFiller.fillPool(this);
}
}
}
}
private ConnectionListener removeForFrequencyCheck() {
if (log.isDebugEnabled()) {
log.debug("Checking for connection within frequency");
}
ConnectionListener cl = null;
for (Iterator iter = connectionListeners.iterator(); iter.hasNext();) {
cl = (ConnectionListener) iter.next();
long lastCheck = cl.getLastValidatedTime();
if ((System.currentTimeMillis() - lastCheck) >= poolParams.backgroundInterval) {
connectionListeners.remove(cl);
break;
} else {
cl = null;
}
}
return cl;
}
private void returnForFrequencyCheck(ConnectionListener cl) {
if (log.isDebugEnabled()) {
log.debug("Returning for connection within frequency");
}
cl.setLastValidatedTime(System.currentTimeMillis());
connectionListeners.add(cl);
}
/**
* Guard against configurations or
* dynamic changes that may increase the minimum
* beyond the maximum
*/
private int getMinSize() {
if (poolParams.minSize > maxSize)
return maxSize;
return poolParams.minSize;
}
/**
*
* @author sicong.shou
* @version $Id: InternalManagedConnectionPool.java, v 0.1 2012-11-23 ����11:49:54 sicong.shou Exp $
*/
public static class PoolParams {
public int minSize = 0;
public int maxSize = 10;
public int blockingTimeout = 30000; //milliseconds
public long idleTimeout = 1000 * 60 * 30; //milliseconds, 30 minutes.
public boolean backgroundValidation; //set to false by default
public long backgroundInterval = 1000 * 60 * 10; //milliseconds, 10 minutes;
public boolean prefill;
//Do we want to immeadiately break when a connection cannot be matched and not evaluate the rest of the pool?
public boolean useFastFail;
}
/**
*
* @author sicong.shou
* @version $Id: InternalManagedConnectionPool.java, v 0.1 2012-11-23 ����11:49:58 sicong.shou Exp $
*/
public static class InternalSemaphore extends Semaphore {
private static final long serialVersionUID = -3263541301167160976L;
public InternalSemaphore(int permits) {
super(permits);
}
public InternalSemaphore(int permits, boolean fair) {
super(permits, fair);
}
public void reduceSemaphores(int reduction) {
super.reducePermits(reduction);
}
}
/**
* Stats
*/
private static class Counter {
private int created = 0;
private int destroyed = 0;
synchronized int getGuaranteedCount() {
return created - destroyed;
}
int getCount() {
return created - destroyed;
}
int getCreatedCount() {
return created;
}
int getDestroyedCount() {
return destroyed;
}
synchronized void inc() {
++created;
}
synchronized void dec() {
++destroyed;
}
}
}