All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.freeutils.util.db.DBConnectionPool Maven / Gradle / Ivy

The newest version!
/*
 *  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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy