package com.j256.ormlite.jdbc;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.j256.ormlite.db.DatabaseType;
import com.j256.ormlite.logger.Log.Level;
import com.j256.ormlite.logger.Logger;
import com.j256.ormlite.logger.LoggerFactory;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.support.DatabaseConnection;
/**
* Implementation of the ConnectionSource interface that supports basic pooled connections. New connections are created
* on demand only if there are no dormant connections otherwise released connections will be reused. This class is
* reentrant and can handle requests from multiple threads.
*
* <p>
* <b> WARNING: </b> As of 10/2010 this is one of the newer parts of ORMLite meaning it may still have bugs. Additional
* review of the code and any feedback would be appreciated.
* </p>
*
* <p>
* <b> NOTE: </b> If you are using the Spring type wiring in Java, {@link #initialize} should be called after all of the
* set methods. In Spring XML, init-method="initialize" should be used.
* </p>
*
* <p>
* <b> NOTE: </b> This class spawns a thread to test the pooled connections that are in the free-list as a keep-alive
* mechanism. It will test any dormant connections every so often to see if they are still valid. If this is not the
* behavior that you want then call {@link #setCheckConnectionsEveryMillis(long)} with 0 to disable the thread. You can
* also call {@link #setTestBeforeGet(boolean)} and set it to true to test the connection before it is handed back to
* you.
* </p>
*
* @author graywatson
*/
public class JdbcPooledConnectionSource extends JdbcConnectionSource implements ConnectionSource {
private static Logger logger = LoggerFactory.getLogger(JdbcPooledConnectionSource.class);
private final static int DEFAULT_MAX_CONNECTIONS_FREE = 5;
// maximum age that a connection can be before being closed
private final static int DEFAULT_MAX_CONNECTION_AGE_MILLIS = 60 * 60 * 1000;
private final static int CHECK_CONNECTIONS_EVERY_MILLIS = 30 * 1000;
private int maxConnectionsFree = DEFAULT_MAX_CONNECTIONS_FREE;
private long maxConnectionAgeMillis = DEFAULT_MAX_CONNECTION_AGE_MILLIS;
private List<ConnectionMetaData> connFreeList = new ArrayList<ConnectionMetaData>();
private Map<DatabaseConnection, ConnectionMetaData> connectionMap =
new HashMap<DatabaseConnection, ConnectionMetaData>();
private final Object lock = new Object();
private ConnectionTester tester = null;
private String pingStatment;
private int openCount = 0;
private int releaseCount = 0;
private int closeCount = 0;
private int maxEverUsed = 0;
private long checkConnectionsEveryMillis = CHECK_CONNECTIONS_EVERY_MILLIS;
private boolean testBeforeGetFromPool = false;
private volatile boolean isOpen = true;
public JdbcPooledConnectionSource() {
// for spring type wiring
}
public JdbcPooledConnectionSource(String url) throws SQLException {
this(url, null, null, null);
}
public JdbcPooledConnectionSource(String url, DatabaseType databaseType) throws SQLException {
this(url, null, null, databaseType);
}
public JdbcPooledConnectionSource(String url, String username, String password) throws SQLException {
this(url, username, password, null);
}
public JdbcPooledConnectionSource(String url, String username, String password, DatabaseType databaseType)
throws SQLException {
super(url, username, password, databaseType);
}
@Override
public void initialize() throws SQLException {
super.initialize();
pingStatment = databaseType.getPingStatement();
}
@Override
public void close() throws SQLException {
checkInitializedSqlException();
logger.debug("closing");
synchronized (lock) {
// close the outstanding connections in the list
for (ConnectionMetaData connMetaData : connFreeList) {
closeConnectionQuietly(connMetaData);
}
connFreeList.clear();
connFreeList = null;
// NOTE: We can't close the ones left in the connectionMap because they may still be in use.
connectionMap.clear();
isOpen = false;
}
}
@Override
public DatabaseConnection getReadOnlyConnection() throws SQLException {
// set the connection to be read-only in JDBC-land? would need to set read-only or read-write
return getReadWriteConnection();
}
@Override
public DatabaseConnection getReadWriteConnection() throws SQLException {
checkInitializedSqlException();
DatabaseConnection conn = getSavedConnection();
if (conn != null) {
return conn;
}
synchronized (lock) {
while (connFreeList.size() > 0) {
// take the first one off of the list
ConnectionMetaData connMetaData = getFreeConnection();
if (connMetaData == null) {
// need to create a new one
} else if (testBeforeGetFromPool && !testConnection(connMetaData)) {
// close expired connection
closeConnectionQuietly(connMetaData);
} else {
logger.debug("reusing connection {}", connMetaData);
return connMetaData.connection;
}
}
// if none in the free list then make a new one
DatabaseConnection connection = makeConnection(logger);
openCount++;
// add it to our connection map
connectionMap.put(connection, new ConnectionMetaData(connection, maxConnectionAgeMillis));
int maxInUse = connectionMap.size();
if (maxInUse > maxEverUsed) {
maxEverUsed = maxInUse;
}
return connection;
}
}
private ConnectionMetaData getFreeConnection() {
synchronized (lock) {
long now = System.currentTimeMillis();
while (connFreeList.size() > 0) {
// take the first one off of the list
ConnectionMetaData connMetaData = connFreeList.remove(0);
// is it already expired
if (connMetaData.isExpired(now)) {
// close expired connection
closeConnectionQuietly(connMetaData);
} else {
return connMetaData;
}
}
}
return null;
}
@Override
public void releaseConnection(DatabaseConnection connection) throws SQLException {
checkInitializedSqlException();
if (isSavedConnection(connection)) {
// ignore the release when we are in a transaction
return;
}
synchronized (lock) {
releaseCount++;
if (connection.isClosed()) {
// it's already closed so just drop it
ConnectionMetaData meta = connectionMap.remove(connection);
if (meta == null) {
logger.debug("dropping already closed unknown connection {}", connection);
} else {
logger.debug("dropping already closed connection {}", meta);
}
return;
}
if (connFreeList == null) {
// if we've already closed the pool then just close the connection
closeConnection(connection);
return;
}
ConnectionMetaData meta = connectionMap.get(connection);
if (meta == null) {
logger.error("should have found connection {} in the map", connection);
closeConnection(connection);
} else {
connFreeList.add(meta);
logger.debug("cache released connection {}", meta);
if (connFreeList.size() > maxConnectionsFree) {
// close the first connection in the queue
meta = connFreeList.remove(0);
logger.debug("cache too full, closing connection {}", meta);
closeConnection(meta.connection);
}
if (checkConnectionsEveryMillis > 0 && tester == null) {
tester = new ConnectionTester();
tester.setName(getClass().getSimpleName() + " connection tester");
tester.setDaemon(true);
tester.start();
}
}
}
}
@Override
public boolean saveSpecialConnection(DatabaseConnection connection) throws SQLException {
checkInitializedIllegalStateException();
boolean saved = saveSpecial(connection);
if (logger.isLevelEnabled(Level.DEBUG)) {
ConnectionMetaData meta = connectionMap.get(connection);
logger.debug("saved special connection {}", meta);
}
return saved;
}
@Override
public void clearSpecialConnection(DatabaseConnection connection) {
checkInitializedIllegalStateException();
boolean cleared = clearSpecial(connection, logger);
if (logger.isLevelEnabled(Level.DEBUG)) {
ConnectionMetaData meta = connectionMap.get(connection);
if (cleared) {
logger.debug("cleared special connection {}", meta);
} else {
logger.debug("special connection {} not saved", meta);
}
}
// release should then called after the clear
}
@Override
public boolean isOpen() {
return isOpen;
}
public void setUsesTransactions(boolean usesTransactions) {
this.usedSpecialConnection = usesTransactions;
}
/**
* Set the number of connections that can be unused in the available list.
*/
public void setMaxConnectionsFree(int maxConnectionsFree) {
this.maxConnectionsFree = maxConnectionsFree;
}
/**
* Set the number of milliseconds that a connection can stay open before being closed. Set to Long.MAX_VALUE to have
* the connections never expire.
*/
public void setMaxConnectionAgeMillis(long maxConnectionAgeMillis) {
this.maxConnectionAgeMillis = maxConnectionAgeMillis;
}
/**
* Return the approximate number of connections opened over the life of the pool.
*/
public int getOpenCount() {
return openCount;
}
/**
* Return the approximate number of connections released over the life of the pool.
*/
public int getReleaseCount() {
return releaseCount;
}
/**
* Return the approximate number of connections closed over the life of the pool.
*/
public int getCloseCount() {
return closeCount;
}
/**
* Return the approximate maximum number of connections in use at one time.
*/
public int getMaxConnectionsEverUsed() {
return maxEverUsed;
}
/**
* Return the number of currently freed connectionsin the free list.
*/
public int getCurrentConnectionsFree() {
synchronized (lock) {
return connFreeList.size();
}
}
/**
* Return the number of current connections that we are tracking.
*/
public int getCurrentConnectionsManaged() {
synchronized (lock) {
return connectionMap.size();
}
}
/**
* There is an internal thread which checks each of the database connections as a keep-alive mechanism. This set the
* number of milliseconds it sleeps between checks -- default is 30000. To disable the checking thread, set this to
* 0 before you start using the connection source.
*/
public void setCheckConnectionsEveryMillis(long checkConnectionsEveryMillis) {
this.checkConnectionsEveryMillis = checkConnectionsEveryMillis;
}
public void setTestBeforeGet(boolean testBeforeGetFromPool) {
this.testBeforeGetFromPool = testBeforeGetFromPool;
}
private void checkInitializedSqlException() throws SQLException {
if (!initialized) {
throw new SQLException(getClass().getSimpleName() + " was not initialized properly");
}
}
private void checkInitializedIllegalStateException() {
if (!initialized) {
throw new IllegalStateException(getClass().getSimpleName() + " was not initialized properly");
}
}
/**
* This should be inside of synchronized (lock) stanza.
*/
private void closeConnection(DatabaseConnection connection) throws SQLException {
// this can return null if we are closing the pool
ConnectionMetaData meta = connectionMap.remove(connection);
connection.close();
logger.debug("closed connection {}", meta);
closeCount++;
}
/**
* Must be called inside of synchronized(lock)
*/
private void closeConnectionQuietly(ConnectionMetaData connMetaData) {
try {
// close expired connection
closeConnection(connMetaData.connection);
} catch (SQLException e) {
// we ignore this
}
}
private boolean testConnection(ConnectionMetaData connMetaData) {
try {
// issue our ping statement
long result = connMetaData.connection.queryForLong(pingStatment);
logger.debug("tested connection {}, got {}", connMetaData, result);
return true;
} catch (Exception e) {
logger.debug(e, "testing connection {} threw exception: {}", connMetaData, e.getMessage());
return false;
}
}
/**
* Class to hold the connection and its meta data.
*/
private static class ConnectionMetaData {
public final DatabaseConnection connection;
private final long expiresMillis;
public ConnectionMetaData(DatabaseConnection connection, long maxConnectionAgeMillis) {
this.connection = connection;
long now = System.currentTimeMillis();
if (maxConnectionAgeMillis > Long.MAX_VALUE - now) {
this.expiresMillis = Long.MAX_VALUE;
} else {
this.expiresMillis = now + maxConnectionAgeMillis;
}
}
public boolean isExpired(long now) {
return (expiresMillis <= now);
}
@Override
public String toString() {
return "#" + hashCode();
}
}
/**
* Tester thread that checks the connections that we have queued to make sure they are still good.
*/
private class ConnectionTester extends Thread {
private Set<ConnectionMetaData> testedSet = new HashSet<ConnectionMetaData>();
@Override
public void run() {
while (checkConnectionsEveryMillis > 0) {
try {
Thread.sleep(checkConnectionsEveryMillis);
if (!testConnections()) {
return;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// quit if we've been interrupted
return;
}
}
}
/**
* Test the connections, returning true if we should continue.
*/
private boolean testConnections() {
// clear our tested set
testedSet.clear();
long now = System.currentTimeMillis();
ConnectionMetaData connMetaData = null;
while (true) {
synchronized (lock) {
if (connFreeList == null) {
if (connMetaData != null) {
closeConnectionQuietly(connMetaData);
}
// we're closed
return false;
}
if (connMetaData != null) {
// we do this so we don't have to double lock in the loop
connFreeList.add(connMetaData);
}
if (connFreeList.isEmpty()) {
// nothing to do
continue;
}
connMetaData = connFreeList.get(0);
if (testedSet.contains(connMetaData)) {
// we are done if we've tested it before on this pass
return true;
}
// otherwise, take the first one off the list
connMetaData = connFreeList.remove(0);
if (connMetaData.isExpired(now)) {
// close expired connection
closeConnectionQuietly(connMetaData);
// don't return the connection to the free list
connMetaData = null;
continue;
}
}
if (testConnection(connMetaData)) {
testedSet.add(connMetaData);
} else {
synchronized (lock) {
closeConnectionQuietly(connMetaData);
connMetaData = null;
}
}
}
}
}
}