/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.common.db;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.PooledConnection;
/**
* Implementation of very light-weight simple JDBC connection pool
* manager.
*/
public class DBConnectionPoolManager {
private static final Logger Log = LoggerFactory.getLogger(
DBConnectionPoolManager.class
);
private ConnectionPoolDataSource dataSource;
private int maxConnections;
private long timeoutMs;
private Semaphore semaphore;
private PoolConnectionEventListener poolConnectionEventListener;
// The following variables must only be accessed within synchronized blocks.
// @GuardedBy("this") could by used in the future.
/**
* List of {@link PooledConnection} that are not active.
*/
private LinkedList<PooledConnection> recycledConnections;
/**
* Count of open connections in this pool.
*/
private int activeConnections;
/**
* Set to true if this connection pool has been disposed.
*/
private boolean isDisposed;
/**
* Purges the connections currently being closed instead of recycling.
* true indicates to purge, false to recycle.
*/
private boolean doPurgeConnection;
/**
* A PooledConnection which is currently within a
* PooledConnection.getConnection() call, or null.
*/
private PooledConnection connectionInTransition;
/**
* Thrown in {@link #getConnection()} or {@link #getValidConnection()} when
* no free connection becomes available within <code>timeout</code> seconds.
*/
public static class TimeoutException extends RuntimeException {
private static final long serialVersionUID = 1;
public TimeoutException () {
super("Timeout while waiting for a free database connection.");
}
public TimeoutException (String msg) {
super(msg);
}
}
/**
* Constructs a DBConnectionPoolManager object with a timeout of 60 seconds.
*
* @param dataSource
* the data source for the connections.
* @param maxConnections
* the maximum number of connections.
*/
public DBConnectionPoolManager(ConnectionPoolDataSource dataSource,
int maxConnections) {
this(dataSource, maxConnections, 60); }
/**
* Constructs a DBConnectionPoolManager object.
*
* @param dataSource
* the data source for the connections.
* @param maxConnections
* the maximum number of connections.
* @param timeout
* the maximum time in seconds to wait for a free connection.
*/
public DBConnectionPoolManager(ConnectionPoolDataSource dataSource,
int maxConnections, int timeout) {
this.dataSource = dataSource;
this.maxConnections = maxConnections;
this.timeoutMs = timeout * 1000L;
if (maxConnections < 1) {
throw new IllegalArgumentException("Invalid maxConnections value.");
}
semaphore = new Semaphore(maxConnections, true);
recycledConnections = new LinkedList<>();
poolConnectionEventListener = new PoolConnectionEventListener();
}
/**
* Closes all unused pooled connections.
*/
public synchronized void dispose() throws SQLException {
if (isDisposed) {
return;
}
isDisposed = true;
SQLException e = null;
while (!recycledConnections.isEmpty()) {
PooledConnection pconn = recycledConnections.remove();
try {
pconn.close();
} catch (SQLException e2) {
if (e == null) {
e = e2;
}
}
}
if (e != null) {
throw e;
}
}
// Due to a bug in checkstyle, it would emit false positives here of the form
// "Unable to get class information for @throws tag '<exn>' (...)".
// This comment disables that check up to the corresponding ON comments below
// CHECKSTYLE OFF: @throws
/**
* Retrieves a connection from the connection pool.
*
* <p>If <code>maxConnections</code> connections are already in use, the method
* waits until a connection becomes available or <code>timeout</code> seconds elapsed.
* When the application is finished using the connection, it must close it
* in order to return it to the pool.
*
* @return
* a new <code>Connection</code> object.
* @throws TimeoutException
* when no connection becomes available within <code>timeout</code> seconds.
*/
public Connection getConnection() throws SQLException {
return getConnection2(timeoutMs);
}
// CHECKSTYLE ON
private Connection getConnection2 (long timeoutMs) throws SQLException {
// This routine is unsynchronized, because semaphore.tryAcquire() may block.
synchronized (this) {
if (isDisposed) {
throw new IllegalStateException("Connection pool has been disposed.");
}
}
try {
if (!semaphore.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
throw new TimeoutException();
}
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while waiting for a " +
"database connection.", e);
}
boolean ok = false;
try {
Connection conn = getConnection3();
ok = true;
return conn;
} finally {
if (!ok) {
semaphore.release();
}
}
}
private synchronized Connection getConnection3() throws SQLException {
// test again within synchronized lock
if (isDisposed) {
throw new IllegalStateException("Connection pool has been disposed.");
}
PooledConnection pconn;
if (!recycledConnections.isEmpty()) {
pconn = recycledConnections.remove();
} else {
pconn = dataSource.getPooledConnection();
pconn.addConnectionEventListener(poolConnectionEventListener);
}
Connection conn;
try {
// The JDBC driver may call ConnectionEventListener.connectionErrorOccurred()
// from within PooledConnection.getConnection(). To detect this within
// disposeConnection(), we temporarily set connectionInTransition.
connectionInTransition = pconn;
conn = pconn.getConnection();
} finally {
connectionInTransition = null;
}
activeConnections++;
assertInnerState();
return conn;
}
// Due to a bug in checkstyle, it would emit false positives here of the form
// "Unable to get class information for @throws tag '<exn>' (...)".
// This comment disables that check up to the corresponding ON comments below
// CHECKSTYLE OFF: @throws
/**
* Retrieves a connection from the connection pool and ensures that it is valid
* by calling {@link Connection#isValid(int)}.
*
* <p>If a connection is not valid, the method tries to get another connection
* until one is valid (or a timeout occurs).
*
* <p>Pooled connections may become invalid when e.g. the database server is
* restarted.
*
* <p>This method is slower than {@link #getConnection()} because the JDBC
* driver has to send an extra command to the database server to test the
* connection.
*
* <p>This method requires Java 1.6 or newer.
*
* @throws TimeoutException
* when no valid connection becomes available within <code>timeout</code>
* seconds.
*/
public Connection getValidConnection() throws TimeoutException {
long time = System.currentTimeMillis();
long timeoutTime = time + timeoutMs;
int triesWithoutDelay = getInactiveConnections() + 1;
while (true) {
Connection conn = getValidConnection2(time, timeoutTime);
if (conn != null) {
return conn;
}
triesWithoutDelay--;
if (triesWithoutDelay <= 0) {
triesWithoutDelay = 0;
try {
Thread.sleep(250);
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while waiting " +
"for a valid database connection.", e);
}
}
time = System.currentTimeMillis();
if (time >= timeoutTime) {
throw new TimeoutException("Timeout while waiting for a " +
"valid database connection.");
}
}
}
// CHECKSTYLE ON
private Connection getValidConnection2 (long time, long timeoutTime) {
long rtime = Math.max(1, timeoutTime - time);
Connection conn;
try {
conn = getConnection2(rtime);
} catch (SQLException e) {
return null;
}
rtime = timeoutTime - System.currentTimeMillis();
int rtimeSecs = Math.max(1, (int) ((rtime + 999) / 1000));
try {
if (conn.isValid(rtimeSecs)) {
return conn;
}
} catch (SQLException e) {
}
// This Exception should never occur. If it nevertheless occurs,
// it's because of an error in the JDBC driver which we ignore and assume
// that the connection is not valid. When isValid() returns false, the JDBC
// driver should have already called connectionErrorOccurred()
// and the PooledConnection has been removed from the pool, i.e. the
// PooledConnection will not be added to recycledConnections when
// Connection.close() is called. But to be sure that this works even with
// a faulty JDBC driver, we call purgeConnection().
purgeConnection(conn);
return null;
}
// Purges the PooledConnection associated with the passed Connection from
// the connection pool.
private synchronized void purgeConnection (Connection conn) {
try {
doPurgeConnection = true;
// (A potential problem of this program logic is that setting the
// doPurgeConnection flag has an effect only if the JDBC driver calls
// connectionClosed() synchronously within Connection.close().)
conn.close();
} catch (SQLException e) {
// ignore exception from close()
} finally {
doPurgeConnection = false;
}
}
private synchronized void recycleConnection (PooledConnection pconn) {
if (isDisposed || doPurgeConnection) {
disposeConnection(pconn);
return;
}
if (activeConnections <= 0) {
throw new AssertionError();
}
activeConnections--;
semaphore.release();
recycledConnections.add(pconn);
assertInnerState();
}
private synchronized void disposeConnection (PooledConnection pconn) {
pconn.removeConnectionEventListener(poolConnectionEventListener);
if (!recycledConnections.remove(pconn) && pconn != connectionInTransition) {
// If the PooledConnection is not in the recycledConnections list
// and is not currently within a PooledConnection.getConnection() call,
// we assume that the connection was active.
if (activeConnections <= 0) {
throw new AssertionError();
}
activeConnections--;
semaphore.release();
}
closeConnectionAndIgnoreException(pconn);
assertInnerState();
}
private void closeConnectionAndIgnoreException (PooledConnection pconn) {
try {
pconn.close();
} catch (SQLException e) {
log("Error while closing database connection: " + e.toString());
}
}
private void log (String msg) {
Log.error(msg);
}
private synchronized void assertInnerState() {
if (activeConnections < 0) {
throw new AssertionError();
}
if (activeConnections + recycledConnections.size() > maxConnections) {
throw new AssertionError();
}
if (activeConnections + semaphore.availablePermits() > maxConnections) {
throw new AssertionError();
}
}
private class PoolConnectionEventListener implements ConnectionEventListener {
public void connectionClosed (ConnectionEvent event) {
PooledConnection pconn = (PooledConnection) event.getSource();
recycleConnection(pconn);
}
public void connectionErrorOccurred (ConnectionEvent event) {
PooledConnection pconn = (PooledConnection) event.getSource();
disposeConnection(pconn);
}
}
/**
* Returns the number of active (open) connections of this pool.
*
* <p>This is the number of <code>Connection</code> objects that have been
* issued by {@link #getConnection()}, for which <code>Connection.close()</code>
* has not yet been called.
*
* @return
* the number of active connections.
**/
public synchronized int getActiveConnections() {
return activeConnections;
}
/**
* Returns the number of inactive (unused) connections in this pool.
*
* <p>This is the number of internally kept recycled connections,
* for which <code>Connection.close()</code> has been called and which
* have not yet been reused.
*
* @return
* the number of inactive connections.
**/
public synchronized int getInactiveConnections() {
return recycledConnections.size();
}
}