com.j256.ormlite.jdbc.JdbcPooledConnectionSource Maven / Gradle / Ivy
package com.j256.ormlite.jdbc;
import java.io.IOException;
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.misc.IOUtils;
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.
*
*
* NOTE: 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.
*
*
*
* NOTE: 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.
*
*
* @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 connFreeList = new ArrayList();
protected final Map connectionMap =
new HashMap();
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 int testLoopCount = 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 IOException {
if (!initialized) {
throw new IOException(getClass().getSimpleName() + " was not initialized properly");
}
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(String tableName) throws SQLException {
// set the connection to be read-only in JDBC-land? would need to set read-only or read-write
return getReadWriteConnection(tableName);
}
@Override
public DatabaseConnection getReadWriteConnection(String tableName) 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;
}
}
@Override
public void releaseConnection(DatabaseConnection connection) throws SQLException {
checkInitializedSqlException();
if (isSavedConnection(connection)) {
// ignore the release when we are in a transaction
return;
}
/*
* If the connection is not close and has auto-commit turned off then we must roll-back any outstanding
* statements and set auto-commit back to true.
*/
boolean isClosed = connection.isClosed();
if (!isClosed && !connection.isAutoCommit()) {
connection.rollback(null);
connection.setAutoCommit(true);
}
synchronized (lock) {
releaseCount++;
if (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 {
meta.noteUsed();
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(String tableName) {
return isOpen;
}
@Override
public boolean isSingleConnection(String tableName) {
return false;
}
/**
* 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 connections in 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;
}
/**
* Mostly for testing purposes to see how many times our test loop ran.
*/
public int getTestLoopCount() {
return testLoopCount;
}
/**
* This should be inside of synchronized (lock) stanza.
*/
protected void closeConnection(DatabaseConnection connection) throws SQLException {
// this can return null if we are closing the pool
ConnectionMetaData meta = connectionMap.remove(connection);
IOUtils.closeThrowSqlException(connection, "SQL connection");
logger.debug("closed connection {}", meta);
closeCount++;
}
/**
* Must be called inside of synchronized(lock)
*/
protected void closeConnectionQuietly(ConnectionMetaData connMetaData) {
try {
// close expired connection
closeConnection(connMetaData.connection);
} catch (SQLException e) {
// we ignore this
}
}
protected boolean testConnection(ConnectionMetaData connMetaData) {
try {
// issue our ping statement
long result = connMetaData.connection.queryForLong(pingStatment);
logger.trace("tested connection {}, got {}", connMetaData, result);
return true;
} catch (Exception e) {
logger.debug(e, "testing connection {} threw exception", connMetaData);
return false;
}
}
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 {
connMetaData.noteUsed();
return connMetaData;
}
}
}
return null;
}
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");
}
}
/**
* Class to hold the connection and its meta data.
*/
protected static class ConnectionMetaData {
public final DatabaseConnection connection;
private final long expiresMillis;
private long lastUsed;
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;
}
this.lastUsed = now;
}
public boolean isExpired(long now) {
return (expiresMillis <= now);
}
public long getLastUsed() {
return lastUsed;
}
public void noteUsed() {
this.lastUsed = System.currentTimeMillis();
}
@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 {
// class field to reduce gc
private Set testedSet = new HashSet();
@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;
boolean closeLast = false;
while (true) {
testLoopCount++;
synchronized (lock) {
if (closeLast) {
if (connMetaData != null) {
closeConnectionQuietly(connMetaData);
connMetaData = null;
}
closeLast = false;
}
if (connFreeList == null) {
// we're closed
return false;
}
// add a tested connection back into the free-list
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, return to sleep and go again
return true;
}
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);
// see if it is expires so it can be closed immediately
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 {
// we close this inside of the synchronized block
closeLast = true;
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy