PooledDataSource.java

  1. /*
  2.  *    Copyright 2009-2024 the original author or authors.
  3.  *
  4.  *    Licensed under the Apache License, Version 2.0 (the "License");
  5.  *    you may not use this file except in compliance with the License.
  6.  *    You may obtain a copy of the License at
  7.  *
  8.  *       https://www.apache.org/licenses/LICENSE-2.0
  9.  *
  10.  *    Unless required by applicable law or agreed to in writing, software
  11.  *    distributed under the License is distributed on an "AS IS" BASIS,
  12.  *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13.  *    See the License for the specific language governing permissions and
  14.  *    limitations under the License.
  15.  */
  16. package org.apache.ibatis.datasource.pooled;

  17. import java.io.PrintWriter;
  18. import java.lang.reflect.InvocationHandler;
  19. import java.lang.reflect.Proxy;
  20. import java.sql.Connection;
  21. import java.sql.DriverManager;
  22. import java.sql.SQLException;
  23. import java.sql.Statement;
  24. import java.util.Properties;
  25. import java.util.concurrent.TimeUnit;
  26. import java.util.concurrent.locks.Condition;
  27. import java.util.concurrent.locks.Lock;
  28. import java.util.concurrent.locks.ReentrantLock;
  29. import java.util.logging.Logger;

  30. import javax.sql.DataSource;

  31. import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
  32. import org.apache.ibatis.logging.Log;
  33. import org.apache.ibatis.logging.LogFactory;

  34. /**
  35.  * This is a simple, synchronous, thread-safe database connection pool.
  36.  *
  37.  * @author Clinton Begin
  38.  */
  39. public class PooledDataSource implements DataSource {

  40.   private static final Log log = LogFactory.getLog(PooledDataSource.class);

  41.   private final PoolState state = new PoolState(this);

  42.   private final UnpooledDataSource dataSource;

  43.   // OPTIONAL CONFIGURATION FIELDS
  44.   protected int poolMaximumActiveConnections = 10;
  45.   protected int poolMaximumIdleConnections = 5;
  46.   protected int poolMaximumCheckoutTime = 20000;
  47.   protected int poolTimeToWait = 20000;
  48.   protected int poolMaximumLocalBadConnectionTolerance = 3;
  49.   protected String poolPingQuery = "NO PING QUERY SET";
  50.   protected boolean poolPingEnabled;
  51.   protected int poolPingConnectionsNotUsedFor;

  52.   private int expectedConnectionTypeCode;

  53.   private final Lock lock = new ReentrantLock();
  54.   private final Condition condition = lock.newCondition();

  55.   public PooledDataSource() {
  56.     dataSource = new UnpooledDataSource();
  57.   }

  58.   public PooledDataSource(UnpooledDataSource dataSource) {
  59.     this.dataSource = dataSource;
  60.   }

  61.   public PooledDataSource(String driver, String url, String username, String password) {
  62.     dataSource = new UnpooledDataSource(driver, url, username, password);
  63.     expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(),
  64.         dataSource.getPassword());
  65.   }

  66.   public PooledDataSource(String driver, String url, Properties driverProperties) {
  67.     dataSource = new UnpooledDataSource(driver, url, driverProperties);
  68.     expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(),
  69.         dataSource.getPassword());
  70.   }

  71.   public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
  72.     dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
  73.     expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(),
  74.         dataSource.getPassword());
  75.   }

  76.   public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
  77.     dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
  78.     expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(),
  79.         dataSource.getPassword());
  80.   }

  81.   @Override
  82.   public Connection getConnection() throws SQLException {
  83.     return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
  84.   }

  85.   @Override
  86.   public Connection getConnection(String username, String password) throws SQLException {
  87.     return popConnection(username, password).getProxyConnection();
  88.   }

  89.   @Override
  90.   public void setLoginTimeout(int loginTimeout) {
  91.     DriverManager.setLoginTimeout(loginTimeout);
  92.   }

  93.   @Override
  94.   public int getLoginTimeout() {
  95.     return DriverManager.getLoginTimeout();
  96.   }

  97.   @Override
  98.   public void setLogWriter(PrintWriter logWriter) {
  99.     DriverManager.setLogWriter(logWriter);
  100.   }

  101.   @Override
  102.   public PrintWriter getLogWriter() {
  103.     return DriverManager.getLogWriter();
  104.   }

  105.   public void setDriver(String driver) {
  106.     dataSource.setDriver(driver);
  107.     forceCloseAll();
  108.   }

  109.   public void setUrl(String url) {
  110.     dataSource.setUrl(url);
  111.     forceCloseAll();
  112.   }

  113.   public void setUsername(String username) {
  114.     dataSource.setUsername(username);
  115.     forceCloseAll();
  116.   }

  117.   public void setPassword(String password) {
  118.     dataSource.setPassword(password);
  119.     forceCloseAll();
  120.   }

  121.   public void setDefaultAutoCommit(boolean defaultAutoCommit) {
  122.     dataSource.setAutoCommit(defaultAutoCommit);
  123.     forceCloseAll();
  124.   }

  125.   public void setDefaultTransactionIsolationLevel(Integer defaultTransactionIsolationLevel) {
  126.     dataSource.setDefaultTransactionIsolationLevel(defaultTransactionIsolationLevel);
  127.     forceCloseAll();
  128.   }

  129.   public void setDriverProperties(Properties driverProps) {
  130.     dataSource.setDriverProperties(driverProps);
  131.     forceCloseAll();
  132.   }

  133.   /**
  134.    * Sets the default network timeout value to wait for the database operation to complete. See
  135.    * {@link Connection#setNetworkTimeout(java.util.concurrent.Executor, int)}
  136.    *
  137.    * @param milliseconds
  138.    *          The time in milliseconds to wait for the database operation to complete.
  139.    *
  140.    * @since 3.5.2
  141.    */
  142.   public void setDefaultNetworkTimeout(Integer milliseconds) {
  143.     dataSource.setDefaultNetworkTimeout(milliseconds);
  144.     forceCloseAll();
  145.   }

  146.   /**
  147.    * The maximum number of active connections.
  148.    *
  149.    * @param poolMaximumActiveConnections
  150.    *          The maximum number of active connections
  151.    */
  152.   public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
  153.     this.poolMaximumActiveConnections = poolMaximumActiveConnections;
  154.     forceCloseAll();
  155.   }

  156.   /**
  157.    * The maximum number of idle connections.
  158.    *
  159.    * @param poolMaximumIdleConnections
  160.    *          The maximum number of idle connections
  161.    */
  162.   public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
  163.     this.poolMaximumIdleConnections = poolMaximumIdleConnections;
  164.     forceCloseAll();
  165.   }

  166.   /**
  167.    * The maximum number of tolerance for bad connection happens in one thread which are applying for new
  168.    * {@link PooledConnection}.
  169.    *
  170.    * @param poolMaximumLocalBadConnectionTolerance
  171.    *          max tolerance for bad connection happens in one thread
  172.    *
  173.    * @since 3.4.5
  174.    */
  175.   public void setPoolMaximumLocalBadConnectionTolerance(int poolMaximumLocalBadConnectionTolerance) {
  176.     this.poolMaximumLocalBadConnectionTolerance = poolMaximumLocalBadConnectionTolerance;
  177.   }

  178.   /**
  179.    * The maximum time a connection can be used before it *may* be given away again.
  180.    *
  181.    * @param poolMaximumCheckoutTime
  182.    *          The maximum time
  183.    */
  184.   public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
  185.     this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
  186.     forceCloseAll();
  187.   }

  188.   /**
  189.    * The time to wait before retrying to get a connection.
  190.    *
  191.    * @param poolTimeToWait
  192.    *          The time to wait
  193.    */
  194.   public void setPoolTimeToWait(int poolTimeToWait) {
  195.     this.poolTimeToWait = poolTimeToWait;
  196.     forceCloseAll();
  197.   }

  198.   /**
  199.    * The query to be used to check a connection.
  200.    *
  201.    * @param poolPingQuery
  202.    *          The query
  203.    */
  204.   public void setPoolPingQuery(String poolPingQuery) {
  205.     this.poolPingQuery = poolPingQuery;
  206.     forceCloseAll();
  207.   }

  208.   /**
  209.    * Determines if the ping query should be used.
  210.    *
  211.    * @param poolPingEnabled
  212.    *          True if we need to check a connection before using it
  213.    */
  214.   public void setPoolPingEnabled(boolean poolPingEnabled) {
  215.     this.poolPingEnabled = poolPingEnabled;
  216.     forceCloseAll();
  217.   }

  218.   /**
  219.    * If a connection has not been used in this many milliseconds, ping the database to make sure the connection is still
  220.    * good.
  221.    *
  222.    * @param milliseconds
  223.    *          the number of milliseconds of inactivity that will trigger a ping
  224.    */
  225.   public void setPoolPingConnectionsNotUsedFor(int milliseconds) {
  226.     this.poolPingConnectionsNotUsedFor = milliseconds;
  227.     forceCloseAll();
  228.   }

  229.   public String getDriver() {
  230.     return dataSource.getDriver();
  231.   }

  232.   public String getUrl() {
  233.     return dataSource.getUrl();
  234.   }

  235.   public String getUsername() {
  236.     return dataSource.getUsername();
  237.   }

  238.   public String getPassword() {
  239.     return dataSource.getPassword();
  240.   }

  241.   public boolean isAutoCommit() {
  242.     return dataSource.isAutoCommit();
  243.   }

  244.   public Integer getDefaultTransactionIsolationLevel() {
  245.     return dataSource.getDefaultTransactionIsolationLevel();
  246.   }

  247.   public Properties getDriverProperties() {
  248.     return dataSource.getDriverProperties();
  249.   }

  250.   /**
  251.    * Gets the default network timeout.
  252.    *
  253.    * @return the default network timeout
  254.    *
  255.    * @since 3.5.2
  256.    */
  257.   public Integer getDefaultNetworkTimeout() {
  258.     return dataSource.getDefaultNetworkTimeout();
  259.   }

  260.   public int getPoolMaximumActiveConnections() {
  261.     return poolMaximumActiveConnections;
  262.   }

  263.   public int getPoolMaximumIdleConnections() {
  264.     return poolMaximumIdleConnections;
  265.   }

  266.   public int getPoolMaximumLocalBadConnectionTolerance() {
  267.     return poolMaximumLocalBadConnectionTolerance;
  268.   }

  269.   public int getPoolMaximumCheckoutTime() {
  270.     return poolMaximumCheckoutTime;
  271.   }

  272.   public int getPoolTimeToWait() {
  273.     return poolTimeToWait;
  274.   }

  275.   public String getPoolPingQuery() {
  276.     return poolPingQuery;
  277.   }

  278.   public boolean isPoolPingEnabled() {
  279.     return poolPingEnabled;
  280.   }

  281.   public int getPoolPingConnectionsNotUsedFor() {
  282.     return poolPingConnectionsNotUsedFor;
  283.   }

  284.   /**
  285.    * Closes all active and idle connections in the pool.
  286.    */
  287.   public void forceCloseAll() {
  288.     lock.lock();
  289.     try {
  290.       expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(),
  291.           dataSource.getPassword());
  292.       for (int i = state.activeConnections.size(); i > 0; i--) {
  293.         try {
  294.           PooledConnection conn = state.activeConnections.remove(i - 1);
  295.           conn.invalidate();

  296.           Connection realConn = conn.getRealConnection();
  297.           if (!realConn.getAutoCommit()) {
  298.             realConn.rollback();
  299.           }
  300.           realConn.close();
  301.         } catch (Exception e) {
  302.           // ignore
  303.         }
  304.       }
  305.       for (int i = state.idleConnections.size(); i > 0; i--) {
  306.         try {
  307.           PooledConnection conn = state.idleConnections.remove(i - 1);
  308.           conn.invalidate();

  309.           Connection realConn = conn.getRealConnection();
  310.           if (!realConn.getAutoCommit()) {
  311.             realConn.rollback();
  312.           }
  313.           realConn.close();
  314.         } catch (Exception e) {
  315.           // ignore
  316.         }
  317.       }
  318.     } finally {
  319.       lock.unlock();
  320.     }
  321.     if (log.isDebugEnabled()) {
  322.       log.debug("PooledDataSource forcefully closed/removed all connections.");
  323.     }
  324.   }

  325.   public PoolState getPoolState() {
  326.     return state;
  327.   }

  328.   private int assembleConnectionTypeCode(String url, String username, String password) {
  329.     return ("" + url + username + password).hashCode();
  330.   }

  331.   protected void pushConnection(PooledConnection conn) throws SQLException {

  332.     lock.lock();
  333.     try {
  334.       state.activeConnections.remove(conn);
  335.       if (conn.isValid()) {
  336.         if (state.idleConnections.size() < poolMaximumIdleConnections
  337.             && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
  338.           state.accumulatedCheckoutTime += conn.getCheckoutTime();
  339.           if (!conn.getRealConnection().getAutoCommit()) {
  340.             conn.getRealConnection().rollback();
  341.           }
  342.           PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
  343.           state.idleConnections.add(newConn);
  344.           newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
  345.           newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
  346.           conn.invalidate();
  347.           if (log.isDebugEnabled()) {
  348.             log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
  349.           }
  350.           condition.signal();
  351.         } else {
  352.           state.accumulatedCheckoutTime += conn.getCheckoutTime();
  353.           if (!conn.getRealConnection().getAutoCommit()) {
  354.             conn.getRealConnection().rollback();
  355.           }
  356.           conn.getRealConnection().close();
  357.           if (log.isDebugEnabled()) {
  358.             log.debug("Closed connection " + conn.getRealHashCode() + ".");
  359.           }
  360.           conn.invalidate();
  361.         }
  362.       } else {
  363.         if (log.isDebugEnabled()) {
  364.           log.debug("A bad connection (" + conn.getRealHashCode()
  365.               + ") attempted to return to the pool, discarding connection.");
  366.         }
  367.         state.badConnectionCount++;
  368.       }
  369.     } finally {
  370.       lock.unlock();
  371.     }
  372.   }

  373.   private PooledConnection popConnection(String username, String password) throws SQLException {
  374.     boolean countedWait = false;
  375.     PooledConnection conn = null;
  376.     long t = System.currentTimeMillis();
  377.     int localBadConnectionCount = 0;

  378.     while (conn == null) {
  379.       lock.lock();
  380.       try {
  381.         if (!state.idleConnections.isEmpty()) {
  382.           // Pool has available connection
  383.           conn = state.idleConnections.remove(0);
  384.           if (log.isDebugEnabled()) {
  385.             log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
  386.           }
  387.         } else if (state.activeConnections.size() < poolMaximumActiveConnections) {
  388.           // Pool does not have available connection and can create a new connection
  389.           conn = new PooledConnection(dataSource.getConnection(), this);
  390.           if (log.isDebugEnabled()) {
  391.             log.debug("Created connection " + conn.getRealHashCode() + ".");
  392.           }
  393.         } else {
  394.           // Cannot create new connection
  395.           PooledConnection oldestActiveConnection = state.activeConnections.get(0);
  396.           long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
  397.           if (longestCheckoutTime > poolMaximumCheckoutTime) {
  398.             // Can claim overdue connection
  399.             state.claimedOverdueConnectionCount++;
  400.             state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
  401.             state.accumulatedCheckoutTime += longestCheckoutTime;
  402.             state.activeConnections.remove(oldestActiveConnection);
  403.             if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
  404.               try {
  405.                 oldestActiveConnection.getRealConnection().rollback();
  406.               } catch (SQLException e) {
  407.                 /*
  408.                  * Just log a message for debug and continue to execute the following statement like nothing happened.
  409.                  * Wrap the bad connection with a new PooledConnection, this will help to not interrupt current
  410.                  * executing thread and give current thread a chance to join the next competition for another valid/good
  411.                  * database connection. At the end of this loop, bad {@link @conn} will be set as null.
  412.                  */
  413.                 log.debug("Bad connection. Could not roll back");
  414.               }
  415.             }
  416.             conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
  417.             conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
  418.             conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
  419.             oldestActiveConnection.invalidate();
  420.             if (log.isDebugEnabled()) {
  421.               log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
  422.             }
  423.           } else {
  424.             // Must wait
  425.             try {
  426.               if (!countedWait) {
  427.                 state.hadToWaitCount++;
  428.                 countedWait = true;
  429.               }
  430.               if (log.isDebugEnabled()) {
  431.                 log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
  432.               }
  433.               long wt = System.currentTimeMillis();
  434.               if (!condition.await(poolTimeToWait, TimeUnit.MILLISECONDS)) {
  435.                 log.debug("Wait failed...");
  436.               }
  437.               state.accumulatedWaitTime += System.currentTimeMillis() - wt;
  438.             } catch (InterruptedException e) {
  439.               // set interrupt flag
  440.               Thread.currentThread().interrupt();
  441.               break;
  442.             }
  443.           }
  444.         }
  445.         if (conn != null) {
  446.           // ping to server and check the connection is valid or not
  447.           if (conn.isValid()) {
  448.             if (!conn.getRealConnection().getAutoCommit()) {
  449.               conn.getRealConnection().rollback();
  450.             }
  451.             conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
  452.             conn.setCheckoutTimestamp(System.currentTimeMillis());
  453.             conn.setLastUsedTimestamp(System.currentTimeMillis());
  454.             state.activeConnections.add(conn);
  455.             state.requestCount++;
  456.             state.accumulatedRequestTime += System.currentTimeMillis() - t;
  457.           } else {
  458.             if (log.isDebugEnabled()) {
  459.               log.debug("A bad connection (" + conn.getRealHashCode()
  460.                   + ") was returned from the pool, getting another connection.");
  461.             }
  462.             state.badConnectionCount++;
  463.             localBadConnectionCount++;
  464.             conn = null;
  465.             if (localBadConnectionCount > poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance) {
  466.               if (log.isDebugEnabled()) {
  467.                 log.debug("PooledDataSource: Could not get a good connection to the database.");
  468.               }
  469.               throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
  470.             }
  471.           }
  472.         }
  473.       } finally {
  474.         lock.unlock();
  475.       }

  476.     }

  477.     if (conn == null) {
  478.       if (log.isDebugEnabled()) {
  479.         log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
  480.       }
  481.       throw new SQLException(
  482.           "PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
  483.     }

  484.     return conn;
  485.   }

  486.   /**
  487.    * Method to check to see if a connection is still usable
  488.    *
  489.    * @param conn
  490.    *          - the connection to check
  491.    *
  492.    * @return True if the connection is still usable
  493.    */
  494.   protected boolean pingConnection(PooledConnection conn) {
  495.     boolean result;

  496.     try {
  497.       result = !conn.getRealConnection().isClosed();
  498.     } catch (SQLException e) {
  499.       if (log.isDebugEnabled()) {
  500.         log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
  501.       }
  502.       result = false;
  503.     }

  504.     if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
  505.         && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
  506.       try {
  507.         if (log.isDebugEnabled()) {
  508.           log.debug("Testing connection " + conn.getRealHashCode() + " ...");
  509.         }
  510.         Connection realConn = conn.getRealConnection();
  511.         try (Statement statement = realConn.createStatement()) {
  512.           statement.executeQuery(poolPingQuery).close();
  513.         }
  514.         if (!realConn.getAutoCommit()) {
  515.           realConn.rollback();
  516.         }
  517.         if (log.isDebugEnabled()) {
  518.           log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
  519.         }
  520.       } catch (Exception e) {
  521.         log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
  522.         try {
  523.           conn.getRealConnection().close();
  524.         } catch (Exception e2) {
  525.           // ignore
  526.         }
  527.         result = false;
  528.         if (log.isDebugEnabled()) {
  529.           log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
  530.         }
  531.       }
  532.     }
  533.     return result;
  534.   }

  535.   /**
  536.    * Unwraps a pooled connection to get to the 'real' connection
  537.    *
  538.    * @param conn
  539.    *          - the pooled connection to unwrap
  540.    *
  541.    * @return The 'real' connection
  542.    */
  543.   public static Connection unwrapConnection(Connection conn) {
  544.     if (Proxy.isProxyClass(conn.getClass())) {
  545.       InvocationHandler handler = Proxy.getInvocationHandler(conn);
  546.       if (handler instanceof PooledConnection) {
  547.         return ((PooledConnection) handler).getRealConnection();
  548.       }
  549.     }
  550.     return conn;
  551.   }

  552.   @Override
  553.   protected void finalize() throws Throwable {
  554.     forceCloseAll();
  555.     super.finalize();
  556.   }

  557.   @Override
  558.   public <T> T unwrap(Class<T> iface) throws SQLException {
  559.     throw new SQLException(getClass().getName() + " is not a wrapper.");
  560.   }

  561.   @Override
  562.   public boolean isWrapperFor(Class<?> iface) {
  563.     return false;
  564.   }

  565.   @Override
  566.   public Logger getParentLogger() {
  567.     return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
  568.   }

  569. }