KeyedCPDSConnectionFactory.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.dbcp2.datasources;

  18. import java.sql.Connection;
  19. import java.sql.ResultSet;
  20. import java.sql.SQLException;
  21. import java.sql.Statement;
  22. import java.time.Duration;
  23. import java.util.Collections;
  24. import java.util.Map;
  25. import java.util.Set;
  26. import java.util.concurrent.ConcurrentHashMap;

  27. import javax.sql.ConnectionEvent;
  28. import javax.sql.ConnectionEventListener;
  29. import javax.sql.ConnectionPoolDataSource;
  30. import javax.sql.PooledConnection;

  31. import org.apache.commons.dbcp2.Utils;
  32. import org.apache.commons.pool2.KeyedObjectPool;
  33. import org.apache.commons.pool2.KeyedPooledObjectFactory;
  34. import org.apache.commons.pool2.PooledObject;
  35. import org.apache.commons.pool2.impl.DefaultPooledObject;

  36. /**
  37.  * A {@link KeyedPooledObjectFactory} that creates {@link PoolableConnection}s.
  38.  *
  39.  * @since 2.0
  40.  */
  41. final class KeyedCPDSConnectionFactory implements KeyedPooledObjectFactory<UserPassKey, PooledConnectionAndInfo>,
  42.         ConnectionEventListener, PooledConnectionManager {

  43.     private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but "
  44.             + "I have no record of the underlying PooledConnection.";

  45.     private final ConnectionPoolDataSource cpds;
  46.     private final String validationQuery;
  47.     private final Duration validationQueryTimeoutDuration;
  48.     private final boolean rollbackAfterValidation;
  49.     private KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool;
  50.     private Duration maxConnLifetime = Duration.ofMillis(-1);

  51.     /**
  52.      * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated.
  53.      */
  54.     private final Set<PooledConnection> validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>());

  55.     /**
  56.      * Map of PooledConnectionAndInfo instances
  57.      */
  58.     private final Map<PooledConnection, PooledConnectionAndInfo> pcMap = new ConcurrentHashMap<>();

  59.     /**
  60.      * Creates a new {@code KeyedPoolableConnectionFactory}.
  61.      *
  62.      * @param cpds
  63.      *            the ConnectionPoolDataSource from which to obtain PooledConnections
  64.      * @param validationQuery
  65.      *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
  66.      *            row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate
  67.      *            connections.
  68.      * @param validationQueryTimeoutSeconds
  69.      *            The Duration to allow for the validation query to complete
  70.      * @param rollbackAfterValidation
  71.      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
  72.      * @since 2.10.0
  73.      */
  74.     public KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
  75.             final Duration validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) {
  76.         this.cpds = cpds;
  77.         this.validationQuery = validationQuery;
  78.         this.validationQueryTimeoutDuration = validationQueryTimeoutSeconds;
  79.         this.rollbackAfterValidation = rollbackAfterValidation;
  80.     }

  81.     /**
  82.      * Creates a new {@code KeyedPoolableConnectionFactory}.
  83.      *
  84.      * @param cpds
  85.      *            the ConnectionPoolDataSource from which to obtain PooledConnections
  86.      * @param validationQuery
  87.      *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
  88.      *            row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate
  89.      *            connections.
  90.      * @param validationQueryTimeoutSeconds
  91.      *            The time, in seconds, to allow for the validation query to complete
  92.      * @param rollbackAfterValidation
  93.      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
  94.      * @deprecated Use {@link #KeyedCPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean)}.
  95.      */
  96.     @Deprecated
  97.     public KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
  98.             final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) {
  99.         this(cpds, validationQuery, Duration.ofSeconds(validationQueryTimeoutSeconds), rollbackAfterValidation);
  100.     }

  101.     @Override
  102.     public void activateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
  103.         validateLifetime(p);
  104.     }

  105.     /**
  106.      * This implementation does not fully close the KeyedObjectPool, as this would affect all users. Instead, it clears
  107.      * the pool associated with the given user. This method is not currently used.
  108.      */
  109.     @Override
  110.     public void closePool(final String userName) throws SQLException {
  111.         try {
  112.             pool.clear(new UserPassKey(userName));
  113.         } catch (final Exception ex) {
  114.             throw new SQLException("Error closing connection pool", ex);
  115.         }
  116.     }

  117.     /**
  118.      * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
  119.      * user calls the close() method of this connection object. What we need to do here is to release this
  120.      * PooledConnection from our pool...
  121.      */
  122.     @Override
  123.     public void connectionClosed(final ConnectionEvent event) {
  124.         final PooledConnection pc = (PooledConnection) event.getSource();
  125.         // if this event occurred because we were validating, or if this
  126.         // connection has been marked for removal, ignore it
  127.         // otherwise return the connection to the pool.
  128.         if (!validatingSet.contains(pc)) {
  129.             final PooledConnectionAndInfo pci = pcMap.get(pc);
  130.             if (pci == null) {
  131.                 throw new IllegalStateException(NO_KEY_MESSAGE);
  132.             }
  133.             try {
  134.                 pool.returnObject(pci.getUserPassKey(), pci);
  135.             } catch (final Exception e) {
  136.                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL");
  137.                 pc.removeConnectionEventListener(this);
  138.                 try {
  139.                     pool.invalidateObject(pci.getUserPassKey(), pci);
  140.                 } catch (final Exception e3) {
  141.                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
  142.                     e3.printStackTrace();
  143.                 }
  144.             }
  145.         }
  146.     }

  147.     /**
  148.      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
  149.      */
  150.     @Override
  151.     public void connectionErrorOccurred(final ConnectionEvent event) {
  152.         final PooledConnection pc = (PooledConnection) event.getSource();
  153.         if (null != event.getSQLException()) {
  154.             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
  155.         }
  156.         pc.removeConnectionEventListener(this);

  157.         final PooledConnectionAndInfo info = pcMap.get(pc);
  158.         if (info == null) {
  159.             throw new IllegalStateException(NO_KEY_MESSAGE);
  160.         }
  161.         try {
  162.             pool.invalidateObject(info.getUserPassKey(), info);
  163.         } catch (final Exception e) {
  164.             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + info);
  165.             e.printStackTrace();
  166.         }
  167.     }

  168.     /**
  169.      * Closes the PooledConnection and stops listening for events from it.
  170.      */
  171.     @Override
  172.     public void destroyObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
  173.         final PooledConnection pooledConnection = p.getObject().getPooledConnection();
  174.         pooledConnection.removeConnectionEventListener(this);
  175.         pcMap.remove(pooledConnection);
  176.         pooledConnection.close();
  177.     }

  178.     /**
  179.      * Returns the keyed object pool used to pool connections created by this factory.
  180.      *
  181.      * @return KeyedObjectPool managing pooled connections
  182.      */
  183.     public KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> getPool() {
  184.         return pool;
  185.     }

  186.     /**
  187.      * Invalidates the PooledConnection in the pool. The KeyedCPDSConnectionFactory closes the connection and pool
  188.      * counters are updated appropriately. Also clears any idle instances associated with the user name that was used to
  189.      * create the PooledConnection. Connections associated with this user are not affected, and they will not be
  190.      * automatically closed on return to the pool.
  191.      */
  192.     @Override
  193.     public void invalidate(final PooledConnection pc) throws SQLException {
  194.         final PooledConnectionAndInfo info = pcMap.get(pc);
  195.         if (info == null) {
  196.             throw new IllegalStateException(NO_KEY_MESSAGE);
  197.         }
  198.         final UserPassKey key = info.getUserPassKey();
  199.         try {
  200.             pool.invalidateObject(key, info); // Destroy and update pool counters
  201.             pool.clear(key); // Remove any idle instances with this key
  202.         } catch (final Exception ex) {
  203.             throw new SQLException("Error invalidating connection", ex);
  204.         }
  205.     }

  206.     /**
  207.      * Creates a new {@code PooledConnectionAndInfo} from the given {@code UserPassKey}.
  208.      *
  209.      * @param userPassKey
  210.      *            {@code UserPassKey} containing user credentials
  211.      * @throws SQLException
  212.      *             if the connection could not be created.
  213.      * @see org.apache.commons.pool2.KeyedPooledObjectFactory#makeObject(Object)
  214.      */
  215.     @Override
  216.     public synchronized PooledObject<PooledConnectionAndInfo> makeObject(final UserPassKey userPassKey) throws SQLException {
  217.         PooledConnection pooledConnection = null;
  218.         final String userName = userPassKey.getUserName();
  219.         final String password = userPassKey.getPassword();
  220.         if (userName == null) {
  221.             pooledConnection = cpds.getPooledConnection();
  222.         } else {
  223.             pooledConnection = cpds.getPooledConnection(userName, password);
  224.         }

  225.         if (pooledConnection == null) {
  226.             throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
  227.         }

  228.         // should we add this object as a listener or the pool.
  229.         // consider the validateObject method in decision
  230.         pooledConnection.addConnectionEventListener(this);
  231.         final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pooledConnection, userPassKey);
  232.         pcMap.put(pooledConnection, pci);

  233.         return new DefaultPooledObject<>(pci);
  234.     }

  235.     @Override
  236.     public void passivateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
  237.         validateLifetime(p);
  238.     }

  239.     /**
  240.      * Sets the maximum lifetime of a connection after which the connection will always fail activation,
  241.      * passivation and validation.
  242.      *
  243.      * @param maxConnLifetimeMillis
  244.      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
  245.      * @since 2.10.0
  246.      */
  247.     public void setMaxConn(final Duration maxConnLifetimeMillis) {
  248.         this.maxConnLifetime = maxConnLifetimeMillis;
  249.     }

  250.     /**
  251.      * Sets the maximum lifetime of a connection after which the connection will always fail activation,
  252.      * passivation and validation.
  253.      *
  254.      * @param maxConnLifetimeMillis
  255.      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
  256.      * @since 2.9.0
  257.      * @deprecated Use {@link #setMaxConn(Duration)}.
  258.      */
  259.     @Deprecated
  260.     public void setMaxConnLifetime(final Duration maxConnLifetimeMillis) {
  261.         this.maxConnLifetime = maxConnLifetimeMillis;
  262.     }

  263.     /**
  264.      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
  265.      * passivation and validation.
  266.      *
  267.      * @param maxConnLifetimeMillis
  268.      *            A value of zero or less indicates an infinite lifetime. The default value is -1.
  269.      * @deprecated Use {@link #setMaxConnLifetime(Duration)}.
  270.      */
  271.     @Deprecated
  272.     public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) {
  273.         setMaxConn(Duration.ofMillis(maxConnLifetimeMillis));
  274.     }

  275.     /**
  276.      * Does nothing. This factory does not cache user credentials.
  277.      */
  278.     @Override
  279.     public void setPassword(final String password) {
  280.         // Does nothing. This factory does not cache user credentials.
  281.     }

  282.     public void setPool(final KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool) {
  283.         this.pool = pool;
  284.     }

  285.     private void validateLifetime(final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
  286.         Utils.validateLifetime(pooledObject, maxConnLifetime);
  287.     }

  288.     /**
  289.      * Validates a pooled connection.
  290.      *
  291.      * @param key
  292.      *            ignored
  293.      * @param pooledObject
  294.      *            wrapped {@code PooledConnectionAndInfo} containing the connection to validate
  295.      * @return true if validation succeeds
  296.      */
  297.     @Override
  298.     public boolean validateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> pooledObject) {
  299.         try {
  300.             validateLifetime(pooledObject);
  301.         } catch (final Exception e) {
  302.             return false;
  303.         }
  304.         boolean valid = false;
  305.         final PooledConnection pooledConn = pooledObject.getObject().getPooledConnection();
  306.         Connection conn = null;
  307.         validatingSet.add(pooledConn);
  308.         if (null == validationQuery) {
  309.             Duration timeoutDuration = validationQueryTimeoutDuration;
  310.             if (timeoutDuration.isNegative()) {
  311.                 timeoutDuration = Duration.ZERO;
  312.             }
  313.             try {
  314.                 conn = pooledConn.getConnection();
  315.                 valid = conn.isValid((int) timeoutDuration.getSeconds());
  316.             } catch (final SQLException e) {
  317.                 valid = false;
  318.             } finally {
  319.                 Utils.closeQuietly((AutoCloseable) conn);
  320.                 validatingSet.remove(pooledConn);
  321.             }
  322.         } else {
  323.             Statement stmt = null;
  324.             ResultSet rset = null;
  325.             // logical Connection from the PooledConnection must be closed
  326.             // before another one can be requested and closing it will
  327.             // generate an event. Keep track so we know not to return
  328.             // the PooledConnection
  329.             validatingSet.add(pooledConn);
  330.             try {
  331.                 conn = pooledConn.getConnection();
  332.                 stmt = conn.createStatement();
  333.                 rset = stmt.executeQuery(validationQuery);
  334.                 valid = rset.next();
  335.                 if (rollbackAfterValidation) {
  336.                     conn.rollback();
  337.                 }
  338.             } catch (final Exception e) {
  339.                 valid = false;
  340.             } finally {
  341.                 Utils.closeQuietly((AutoCloseable) rset);
  342.                 Utils.closeQuietly((AutoCloseable) stmt);
  343.                 Utils.closeQuietly((AutoCloseable) conn);
  344.                 validatingSet.remove(pooledConn);
  345.             }
  346.         }
  347.         return valid;
  348.     }
  349. }