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