net.freeutils.util.db.DBConnectionPool Maven / Gradle / Ivy
Show all versions of jelementary Show documentation
/*
* Copyright © 2003-2024 Amichai Rothman
*
* This file is part of JElementary - the Java Elementary Utilities package.
*
* JElementary is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* JElementary is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with JElementary. If not, see .
*
* For additional info see https://www.freeutils.net/source/jelementary/
*/
package net.freeutils.util.db;
import java.io.Closeable;
import java.lang.reflect.Method;
import java.sql.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sql.DataSource;
import net.freeutils.util.ConcurrentStack;
import net.freeutils.util.DelegatingProxy;
/**
* The {@code DBConnectionPool} class provides database connection pooling.
*
* @author Yaniv Kunda
* @author Amichai Rothman
* @since 2009-02-14
*/
public class DBConnectionPool implements Closeable {
/**
* Wraps a pooled Connection.
*/
protected class ConnectionWrapper extends DelegatingProxy {
/**
* Wraps a cached PreparedStatement.
*/
protected class PreparedStatementWrapper extends DelegatingProxy {
/**
* Constructs a {@link PreparedStatementWrapper}.
*
* @param statement the underlying statement
*/
public PreparedStatementWrapper(PreparedStatement statement) {
super(true, statement, PreparedStatement.class);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return super.invoke(proxy, method, args);
} catch (SQLException sqle) {
exception = sqle;
throw sqle;
}
}
/**
* Returns the proxy of the connection that created this statement.
* (instead of the underlying connection).
*
* @return the proxy of the connection that created this statement
* @throws SQLException if an error occurs
*/
public Connection getConnection() throws SQLException {
// must declare throwing an exception to match proxied signature
return ConnectionWrapper.this.getProxy();
}
/**
* Releases the statement and clears its state for future reuse from the cache.
*
* @throws SQLException if an error occurs
*/
public void close() throws SQLException {
try {
target.clearParameters();
target.clearWarnings();
target.clearBatch();
// note: according to the Statement API, every execution of a statement
// implicitly closes any open result sets, and it's good practice for
// the user to close it anyway, so no need to close them explicitly here
} catch (SQLException sqle) {
reallyClose();
}
}
/**
* Closes the statement and removes it from the pool.
*/
public void reallyClose() {
statements.values().remove(this);
// when the Statement is closed, any open ResultSet is also closed
DBManager.closeSilently(target);
}
}
private final ConcurrentMap statements = new ConcurrentHashMap<>();
protected volatile Exception exception;
protected volatile long lastUsedTime;
protected volatile boolean autoCommit = true; // JDBC sets auto-commit on by default
/**
* Constructs a {@link ConnectionWrapper}.
*
* @param conn the underlying physical connection
*/
public ConnectionWrapper(Connection conn) {
super(true, conn, Connection.class);
}
/**
* Updates the connection's last used time to the current time.
*/
protected void updateLastUsedTime() {
lastUsedTime = System.currentTimeMillis();
}
/**
* Returns the connection's last used time.
*
* @return the connection's last used time
*/
protected long getLastUsedTime() {
return lastUsedTime;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Object result = super.invoke(proxy, method, args);
if (method.getDeclaringClass() == Connection.class) // ignore equals/hashCode/toString
updateLastUsedTime();
return result;
} catch (SQLException sqle) {
exception = sqle;
throw sqle;
}
}
/**
* Returns the connection's auto-commit setting.
*
* @return the connection's auto-commit setting
* @throws SQLException if an error occurs
*/
public boolean getAutoCommit() throws SQLException {
return autoCommit;
}
/**
* Sets the connection's auto-commit setting.
*
* This is an optimization which caches the current auto-commit mode
* of the connection, so that if the desired mode is already set
* there is no access to the underlying connection or database.
*
* @param autoCommit specifies whether auto-commit is enabled or not
* @throws SQLException if an error occurs
*/
public void setAutoCommit(boolean autoCommit) throws SQLException {
if (this.autoCommit != autoCommit) {
getTarget().setAutoCommit(autoCommit); // set on real connection
this.autoCommit = autoCommit;
}
}
/**
* Prepares a statement for the given query.
*
* If a statement for the query is already cached, it is returned. Otherwise,
* a new statement is prepared, added to the cache, and returned.
*
* @param query the query
* @return a statement for the given query
* @throws SQLException if an error occurs
*/
public PreparedStatement prepareStatement(String query) throws SQLException {
// note: this method is called from multiple threads, but not concurrently
PreparedStatementWrapper psw = statements.get(query);
if (psw == null) {
psw = new PreparedStatementWrapper(target.prepareStatement(query));
statements.put(query, psw);
}
return psw.getProxy();
}
/**
* Releases the connection and returns it to the pool.
*
* @throws SQLException if an error occurs
*/
public void close() throws SQLException {
boolean reuse = false;
try {
reuse = exception == null || DBManager.testConnection(target);
if (!reuse)
reallyClose();
} finally {
exception = null;
releaseConnectionWrapper(this, reuse);
}
}
/**
* Closes the connection and removes it from the pool.
*/
public void reallyClose() {
// note: make sure the connection is not in pool at this point
// remove and close all cached statements
for (PreparedStatementWrapper psw : statements.values())
psw.reallyClose();
// close connection
DBManager.closeSilently(target);
}
}
protected final DataSource source;
protected final long timeout;
protected final long idleConnectionTestTimeout = 10 * 60 * 1000; // 10 minutes
protected final int maxConnections;
protected final ConcurrentStack pool;
protected final Semaphore sem;
protected final AtomicBoolean closed = new AtomicBoolean();
/**
* Creates a new DBConnectionPool.
*
* @param source the DataSource creating the connections
* @param timeout the timeout to wait for an available connection
* @param maxConnections the maximum number of connections in the pool
*/
public DBConnectionPool(DataSource source, long timeout, int maxConnections) {
this.source = source;
this.timeout = timeout;
this.maxConnections = maxConnections;
this.pool = new ConcurrentStack<>(maxConnections);
this.sem = new Semaphore(maxConnections, true);
}
/**
* Returns a connection.
*
*
* - If a connection is available, it is returned immediately
*
- If there is no available connection,
*
* - if the maximum number of connections has not been reached,
* a new connection is created, added to the pool and returned.
*
- if the maximum number of connections has been reached,
* this method blocks until a connection becomes available,
* the timeout expires, or the calling thread is interrupted.
*
*
*
* @return a connection
* @throws InterruptedException if the calling thread is interrupted
* @throws TimeoutException if the timeout expired and no connection became available
* @throws SQLException if an error occurs creating or getting the connection,
* or the pool has been closed
*/
public Connection getConnection() throws InterruptedException, TimeoutException, SQLException {
if (closed.get())
throw new SQLException("pool is closed");
if (!sem.tryAcquire(timeout, TimeUnit.MILLISECONDS))
throw new TimeoutException("timed out waiting for connection");
Connection conn = null;
try {
conn = acquireConnectionWrapper().getProxy();
} finally {
if (conn == null) // an exception was thrown before assignment
sem.release();
}
return conn;
}
/**
* Returns a connection wrapper by removing it from the head of the pool,
* or creating a new one if the pool is empty.
*
* @return a connection
* @throws SQLException if an error occurs creating the connection
*/
private ConnectionWrapper acquireConnectionWrapper() throws SQLException {
ConnectionWrapper cw;
// find the first available connection that is either
// recently active or passes a connection test
do {
cw = pool.poll();
} while (cw != null
&& System.currentTimeMillis() - cw.getLastUsedTime() > idleConnectionTestTimeout
&& !DBManager.testConnection(cw.getProxy()));
// note: a newly created connection is not added to the pool
// but only returned, just as an existing one would be removed
// from the pool and returned; they are both added when released.
if (cw == null)
cw = new ConnectionWrapper(source.getConnection());
return cw;
}
/**
* Releases a connection wrapper, optionally returning it to the pool.
*
* This method must be called exactly once for each successful call to
* {@link #getConnection()}.
*
* @param wrapper the connection wrapper to release
* @param returnToPool specifies whether the connection should be returned
* to the pool, or released for good
*/
private void releaseConnectionWrapper(ConnectionWrapper wrapper, boolean returnToPool) {
try {
if (returnToPool)
pool.offer(wrapper);
} finally {
sem.release();
}
}
/**
* Closes this connection pool. This method blocks until all connections have
* been returned to the pool and then closes them.
*
* Note: a pool cannot be used after it is closed.
*/
@Override
public void close() {
// shut the gates to the pool
if (!closed.compareAndSet(false, true))
return;
// steal all permits to lock everyone else out,
// and make sure all connections have been returned to pool
try {
boolean done = sem.tryAcquire(maxConnections, timeout, TimeUnit.MILLISECONDS);
if (!done)
throw new RuntimeException(String.format(
"timeout waiting for %d of the %d pooled connections to be released. " +
"This may indicate a leak (connections not closed properly), " +
"a thread that is stuck while holding the connection, " +
"or a query that is taking too long to complete",
maxConnections - sem.availablePermits(), maxConnections));
} catch (InterruptedException ignore) {}
// close all connections
for (ConnectionWrapper cw = pool.poll(); cw != null; cw = pool.poll())
cw.reallyClose();
}
}