View Javadoc
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  
19  import java.sql.Connection;
20  import java.sql.ResultSet;
21  import java.sql.SQLException;
22  import java.sql.Statement;
23  import java.time.Duration;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.Set;
27  import java.util.concurrent.ConcurrentHashMap;
28  
29  import javax.sql.ConnectionEvent;
30  import javax.sql.ConnectionEventListener;
31  import javax.sql.ConnectionPoolDataSource;
32  import javax.sql.PooledConnection;
33  
34  import org.apache.commons.dbcp2.Utils;
35  import org.apache.commons.pool2.KeyedObjectPool;
36  import org.apache.commons.pool2.KeyedPooledObjectFactory;
37  import org.apache.commons.pool2.PooledObject;
38  import org.apache.commons.pool2.impl.DefaultPooledObject;
39  
40  /**
41   * A {@link KeyedPooledObjectFactory} that creates {@link PoolableConnection}s.
42   *
43   * @since 2.0
44   */
45  final class KeyedCPDSConnectionFactory implements KeyedPooledObjectFactory<UserPassKey, PooledConnectionAndInfo>,
46          ConnectionEventListener, PooledConnectionManager {
47  
48      private static final String NO_KEY_MESSAGE = "close() was called on a Connection, but "
49              + "I have no record of the underlying PooledConnection.";
50  
51      private final ConnectionPoolDataSource cpds;
52      private final String validationQuery;
53      private final Duration validationQueryTimeoutDuration;
54      private final boolean rollbackAfterValidation;
55      private KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool;
56      private Duration maxConnLifetime = Duration.ofMillis(-1);
57  
58      /**
59       * Map of PooledConnections for which close events are ignored. Connections are muted when they are being validated.
60       */
61      private final Set<PooledConnection> validatingSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
62  
63      /**
64       * Map of PooledConnectionAndInfo instances
65       */
66      private final Map<PooledConnection, PooledConnectionAndInfo> pcMap = new ConcurrentHashMap<>();
67  
68      /**
69       * Creates a new {@code KeyedPoolableConnectionFactory}.
70       *
71       * @param cpds
72       *            the ConnectionPoolDataSource from which to obtain PooledConnections
73       * @param validationQuery
74       *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
75       *            row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate
76       *            connections.
77       * @param validationQueryTimeoutSeconds
78       *            The Duration to allow for the validation query to complete
79       * @param rollbackAfterValidation
80       *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
81       * @since 2.10.0
82       */
83      public KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
84              final Duration validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) {
85          this.cpds = cpds;
86          this.validationQuery = validationQuery;
87          this.validationQueryTimeoutDuration = validationQueryTimeoutSeconds;
88          this.rollbackAfterValidation = rollbackAfterValidation;
89      }
90  
91      /**
92       * Creates a new {@code KeyedPoolableConnectionFactory}.
93       *
94       * @param cpds
95       *            the ConnectionPoolDataSource from which to obtain PooledConnections
96       * @param validationQuery
97       *            a query to use to {@link #validateObject validate} {@link Connection}s. Should return at least one
98       *            row. May be {@code null} in which case3 {@link Connection#isValid(int)} will be used to validate
99       *            connections.
100      * @param validationQueryTimeoutSeconds
101      *            The time, in seconds, to allow for the validation query to complete
102      * @param rollbackAfterValidation
103      *            whether a rollback should be issued after {@link #validateObject validating} {@link Connection}s.
104      * @deprecated Use {@link #KeyedCPDSConnectionFactory(ConnectionPoolDataSource, String, Duration, boolean)}.
105      */
106     @Deprecated
107     public KeyedCPDSConnectionFactory(final ConnectionPoolDataSource cpds, final String validationQuery,
108             final int validationQueryTimeoutSeconds, final boolean rollbackAfterValidation) {
109         this(cpds, validationQuery, Duration.ofSeconds(validationQueryTimeoutSeconds), rollbackAfterValidation);
110     }
111 
112     @Override
113     public void activateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
114         validateLifetime(p);
115     }
116 
117     /**
118      * This implementation does not fully close the KeyedObjectPool, as this would affect all users. Instead, it clears
119      * the pool associated with the given user. This method is not currently used.
120      */
121     @Override
122     public void closePool(final String userName) throws SQLException {
123         try {
124             pool.clear(new UserPassKey(userName));
125         } catch (final Exception ex) {
126             throw new SQLException("Error closing connection pool", ex);
127         }
128     }
129 
130     /**
131      * This will be called if the Connection returned by the getConnection method came from a PooledConnection, and the
132      * user calls the close() method of this connection object. What we need to do here is to release this
133      * PooledConnection from our pool...
134      */
135     @Override
136     public void connectionClosed(final ConnectionEvent event) {
137         final PooledConnection pc = (PooledConnection) event.getSource();
138         // if this event occurred because we were validating, or if this
139         // connection has been marked for removal, ignore it
140         // otherwise return the connection to the pool.
141         if (!validatingSet.contains(pc)) {
142             final PooledConnectionAndInfo pci = pcMap.get(pc);
143             if (pci == null) {
144                 throw new IllegalStateException(NO_KEY_MESSAGE);
145             }
146             try {
147                 pool.returnObject(pci.getUserPassKey(), pci);
148             } catch (final Exception e) {
149                 System.err.println("CLOSING DOWN CONNECTION AS IT COULD " + "NOT BE RETURNED TO THE POOL");
150                 pc.removeConnectionEventListener(this);
151                 try {
152                     pool.invalidateObject(pci.getUserPassKey(), pci);
153                 } catch (final Exception e3) {
154                     System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + pci);
155                     e3.printStackTrace();
156                 }
157             }
158         }
159     }
160 
161     /**
162      * If a fatal error occurs, close the underlying physical connection so as not to be returned in the future
163      */
164     @Override
165     public void connectionErrorOccurred(final ConnectionEvent event) {
166         final PooledConnection pc = (PooledConnection) event.getSource();
167         if (null != event.getSQLException()) {
168             System.err.println("CLOSING DOWN CONNECTION DUE TO INTERNAL ERROR (" + event.getSQLException() + ")");
169         }
170         pc.removeConnectionEventListener(this);
171 
172         final PooledConnectionAndInfo info = pcMap.get(pc);
173         if (info == null) {
174             throw new IllegalStateException(NO_KEY_MESSAGE);
175         }
176         try {
177             pool.invalidateObject(info.getUserPassKey(), info);
178         } catch (final Exception e) {
179             System.err.println("EXCEPTION WHILE DESTROYING OBJECT " + info);
180             e.printStackTrace();
181         }
182     }
183 
184     /**
185      * Closes the PooledConnection and stops listening for events from it.
186      */
187     @Override
188     public void destroyObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
189         final PooledConnection pooledConnection = p.getObject().getPooledConnection();
190         pooledConnection.removeConnectionEventListener(this);
191         pcMap.remove(pooledConnection);
192         pooledConnection.close();
193     }
194 
195     /**
196      * Returns the keyed object pool used to pool connections created by this factory.
197      *
198      * @return KeyedObjectPool managing pooled connections
199      */
200     public KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> getPool() {
201         return pool;
202     }
203 
204     /**
205      * Invalidates the PooledConnection in the pool. The KeyedCPDSConnectionFactory closes the connection and pool
206      * counters are updated appropriately. Also clears any idle instances associated with the user name that was used to
207      * create the PooledConnection. Connections associated with this user are not affected, and they will not be
208      * automatically closed on return to the pool.
209      */
210     @Override
211     public void invalidate(final PooledConnection pc) throws SQLException {
212         final PooledConnectionAndInfo info = pcMap.get(pc);
213         if (info == null) {
214             throw new IllegalStateException(NO_KEY_MESSAGE);
215         }
216         final UserPassKey key = info.getUserPassKey();
217         try {
218             pool.invalidateObject(key, info); // Destroy and update pool counters
219             pool.clear(key); // Remove any idle instances with this key
220         } catch (final Exception ex) {
221             throw new SQLException("Error invalidating connection", ex);
222         }
223     }
224 
225     /**
226      * Creates a new {@code PooledConnectionAndInfo} from the given {@code UserPassKey}.
227      *
228      * @param userPassKey
229      *            {@code UserPassKey} containing user credentials
230      * @throws SQLException
231      *             if the connection could not be created.
232      * @see org.apache.commons.pool2.KeyedPooledObjectFactory#makeObject(Object)
233      */
234     @Override
235     public synchronized PooledObject<PooledConnectionAndInfo> makeObject(final UserPassKey userPassKey) throws SQLException {
236         PooledConnection pooledConnection = null;
237         final String userName = userPassKey.getUserName();
238         final String password = userPassKey.getPassword();
239         if (userName == null) {
240             pooledConnection = cpds.getPooledConnection();
241         } else {
242             pooledConnection = cpds.getPooledConnection(userName, password);
243         }
244 
245         if (pooledConnection == null) {
246             throw new IllegalStateException("Connection pool data source returned null from getPooledConnection");
247         }
248 
249         // should we add this object as a listener or the pool.
250         // consider the validateObject method in decision
251         pooledConnection.addConnectionEventListener(this);
252         final PooledConnectionAndInfo pci = new PooledConnectionAndInfo(pooledConnection, userPassKey);
253         pcMap.put(pooledConnection, pci);
254 
255         return new DefaultPooledObject<>(pci);
256     }
257 
258     @Override
259     public void passivateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> p) throws SQLException {
260         validateLifetime(p);
261     }
262 
263     /**
264      * Sets the maximum lifetime 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 milliseconds.
269      * @since 2.10.0
270      */
271     public void setMaxConn(final Duration maxConnLifetimeMillis) {
272         this.maxConnLifetime = maxConnLifetimeMillis;
273     }
274 
275     /**
276      * Sets the maximum lifetime of a connection after which the connection will always fail activation,
277      * passivation and validation.
278      *
279      * @param maxConnLifetimeMillis
280      *            A value of zero or less indicates an infinite lifetime. The default value is -1 milliseconds.
281      * @since 2.9.0
282      * @deprecated Use {@link #setMaxConn(Duration)}.
283      */
284     @Deprecated
285     public void setMaxConnLifetime(final Duration maxConnLifetimeMillis) {
286         this.maxConnLifetime = maxConnLifetimeMillis;
287     }
288 
289     /**
290      * Sets the maximum lifetime in milliseconds of a connection after which the connection will always fail activation,
291      * passivation and validation.
292      *
293      * @param maxConnLifetimeMillis
294      *            A value of zero or less indicates an infinite lifetime. The default value is -1.
295      * @deprecated Use {@link #setMaxConnLifetime(Duration)}.
296      */
297     @Deprecated
298     public void setMaxConnLifetimeMillis(final long maxConnLifetimeMillis) {
299         setMaxConn(Duration.ofMillis(maxConnLifetimeMillis));
300     }
301 
302     /**
303      * Does nothing. This factory does not cache user credentials.
304      */
305     @Override
306     public void setPassword(final String password) {
307         // Does nothing. This factory does not cache user credentials.
308     }
309 
310     public void setPool(final KeyedObjectPool<UserPassKey, PooledConnectionAndInfo> pool) {
311         this.pool = pool;
312     }
313 
314     private void validateLifetime(final PooledObject<PooledConnectionAndInfo> pooledObject) throws SQLException {
315         Utils.validateLifetime(pooledObject, maxConnLifetime);
316     }
317 
318     /**
319      * Validates a pooled connection.
320      *
321      * @param key
322      *            ignored
323      * @param pooledObject
324      *            wrapped {@code PooledConnectionAndInfo} containing the connection to validate
325      * @return true if validation succeeds
326      */
327     @Override
328     public boolean validateObject(final UserPassKey key, final PooledObject<PooledConnectionAndInfo> pooledObject) {
329         try {
330             validateLifetime(pooledObject);
331         } catch (final Exception e) {
332             return false;
333         }
334         boolean valid = false;
335         final PooledConnection pooledConn = pooledObject.getObject().getPooledConnection();
336         Connection conn = null;
337         validatingSet.add(pooledConn);
338         if (null == validationQuery) {
339             Duration timeoutDuration = validationQueryTimeoutDuration;
340             if (timeoutDuration.isNegative()) {
341                 timeoutDuration = Duration.ZERO;
342             }
343             try {
344                 conn = pooledConn.getConnection();
345                 valid = conn.isValid((int) timeoutDuration.getSeconds());
346             } catch (final SQLException e) {
347                 valid = false;
348             } finally {
349                 Utils.closeQuietly((AutoCloseable) conn);
350                 validatingSet.remove(pooledConn);
351             }
352         } else {
353             Statement stmt = null;
354             ResultSet rset = null;
355             // logical Connection from the PooledConnection must be closed
356             // before another one can be requested and closing it will
357             // generate an event. Keep track so we know not to return
358             // the PooledConnection
359             validatingSet.add(pooledConn);
360             try {
361                 conn = pooledConn.getConnection();
362                 stmt = conn.createStatement();
363                 rset = stmt.executeQuery(validationQuery);
364                 valid = rset.next();
365                 if (rollbackAfterValidation) {
366                     conn.rollback();
367                 }
368             } catch (final Exception e) {
369                 valid = false;
370             } finally {
371                 Utils.closeQuietly((AutoCloseable) rset);
372                 Utils.closeQuietly((AutoCloseable) stmt);
373                 Utils.closeQuietly((AutoCloseable) conn);
374                 validatingSet.remove(pooledConn);
375             }
376         }
377         return valid;
378     }
379 }