View Javadoc
1   /**
2    *    Copyright 2009-2020 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    *       http://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  
18  import java.io.PrintWriter;
19  import java.lang.reflect.InvocationHandler;
20  import java.lang.reflect.Proxy;
21  import java.sql.Connection;
22  import java.sql.DriverManager;
23  import java.sql.SQLException;
24  import java.sql.Statement;
25  import java.util.Properties;
26  import java.util.logging.Logger;
27  
28  import javax.sql.DataSource;
29  
30  import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
31  import org.apache.ibatis.logging.Log;
32  import org.apache.ibatis.logging.LogFactory;
33  
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  
41    private static final Log log = LogFactory.getLog(PooledDataSource.class);
42  
43    private final PoolStatee/pooled/PoolState.html#PoolState">PoolState state = new PoolState(this);
44  
45    private final UnpooledDataSource dataSource;
46  
47    // OPTIONAL CONFIGURATION FIELDS
48    protected int poolMaximumActiveConnections = 10;
49    protected int poolMaximumIdleConnections = 5;
50    protected int poolMaximumCheckoutTime = 20000;
51    protected int poolTimeToWait = 20000;
52    protected int poolMaximumLocalBadConnectionTolerance = 3;
53    protected String poolPingQuery = "NO PING QUERY SET";
54    protected boolean poolPingEnabled;
55    protected int poolPingConnectionsNotUsedFor;
56  
57    private int expectedConnectionTypeCode;
58  
59    public PooledDataSource() {
60      dataSource = new UnpooledDataSource();
61    }
62  
63    public PooledDataSource(UnpooledDataSource dataSource) {
64      this.dataSource = dataSource;
65    }
66  
67    public PooledDataSource(String driver, String url, String username, String password) {
68      dataSource = new UnpooledDataSource(driver, url, username, password);
69      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
70    }
71  
72    public PooledDataSource(String driver, String url, Properties driverProperties) {
73      dataSource = new UnpooledDataSource(driver, url, driverProperties);
74      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
75    }
76  
77    public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, String username, String password) {
78      dataSource = new UnpooledDataSource(driverClassLoader, driver, url, username, password);
79      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
80    }
81  
82    public PooledDataSource(ClassLoader driverClassLoader, String driver, String url, Properties driverProperties) {
83      dataSource = new UnpooledDataSource(driverClassLoader, driver, url, driverProperties);
84      expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
85    }
86  
87    @Override
88    public Connection getConnection() throws SQLException {
89      return popConnection(dataSource.getUsername(), dataSource.getPassword()).getProxyConnection();
90    }
91  
92    @Override
93    public Connection getConnection(String username, String password) throws SQLException {
94      return popConnection(username, password).getProxyConnection();
95    }
96  
97    @Override
98    public void setLoginTimeout(int loginTimeout) {
99      DriverManager.setLoginTimeout(loginTimeout);
100   }
101 
102   @Override
103   public int getLoginTimeout() {
104     return DriverManager.getLoginTimeout();
105   }
106 
107   @Override
108   public void setLogWriter(PrintWriter logWriter) {
109     DriverManager.setLogWriter(logWriter);
110   }
111 
112   @Override
113   public PrintWriter getLogWriter() {
114     return DriverManager.getLogWriter();
115   }
116 
117   public void setDriver(String driver) {
118     dataSource.setDriver(driver);
119     forceCloseAll();
120   }
121 
122   public void setUrl(String url) {
123     dataSource.setUrl(url);
124     forceCloseAll();
125   }
126 
127   public void setUsername(String username) {
128     dataSource.setUsername(username);
129     forceCloseAll();
130   }
131 
132   public void setPassword(String password) {
133     dataSource.setPassword(password);
134     forceCloseAll();
135   }
136 
137   public void setDefaultAutoCommit(boolean defaultAutoCommit) {
138     dataSource.setAutoCommit(defaultAutoCommit);
139     forceCloseAll();
140   }
141 
142   public void setDefaultTransactionIsolationLevel(Integer defaultTransactionIsolationLevel) {
143     dataSource.setDefaultTransactionIsolationLevel(defaultTransactionIsolationLevel);
144     forceCloseAll();
145   }
146 
147   public void setDriverProperties(Properties driverProps) {
148     dataSource.setDriverProperties(driverProps);
149     forceCloseAll();
150   }
151 
152   /**
153    * Sets the default network timeout value to wait for the database operation to complete. See {@link Connection#setNetworkTimeout(java.util.concurrent.Executor, int)}
154    *
155    * @param milliseconds
156    *          The time in milliseconds to wait for the database operation to complete.
157    * @since 3.5.2
158    */
159   public void setDefaultNetworkTimeout(Integer milliseconds) {
160     dataSource.setDefaultNetworkTimeout(milliseconds);
161     forceCloseAll();
162   }
163 
164   /**
165    * The maximum number of active connections.
166    *
167    * @param poolMaximumActiveConnections The maximum number of active connections
168    */
169   public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
170     this.poolMaximumActiveConnections = poolMaximumActiveConnections;
171     forceCloseAll();
172   }
173 
174   /**
175    * The maximum number of idle connections.
176    *
177    * @param poolMaximumIdleConnections The maximum number of idle connections
178    */
179   public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
180     this.poolMaximumIdleConnections = poolMaximumIdleConnections;
181     forceCloseAll();
182   }
183 
184   /**
185    * The maximum number of tolerance for bad connection happens in one thread
186    * which are applying for new {@link PooledConnection}.
187    *
188    * @param poolMaximumLocalBadConnectionTolerance
189    * max tolerance for bad connection happens in one thread
190    *
191    * @since 3.4.5
192    */
193   public void setPoolMaximumLocalBadConnectionTolerance(
194       int poolMaximumLocalBadConnectionTolerance) {
195     this.poolMaximumLocalBadConnectionTolerance = poolMaximumLocalBadConnectionTolerance;
196   }
197 
198   /**
199    * The maximum time a connection can be used before it *may* be
200    * given away again.
201    *
202    * @param poolMaximumCheckoutTime The maximum time
203    */
204   public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
205     this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
206     forceCloseAll();
207   }
208 
209   /**
210    * The time to wait before retrying to get a connection.
211    *
212    * @param poolTimeToWait The time to wait
213    */
214   public void setPoolTimeToWait(int poolTimeToWait) {
215     this.poolTimeToWait = poolTimeToWait;
216     forceCloseAll();
217   }
218 
219   /**
220    * The query to be used to check a connection.
221    *
222    * @param poolPingQuery The query
223    */
224   public void setPoolPingQuery(String poolPingQuery) {
225     this.poolPingQuery = poolPingQuery;
226     forceCloseAll();
227   }
228 
229   /**
230    * Determines if the ping query should be used.
231    *
232    * @param poolPingEnabled True if we need to check a connection before using it
233    */
234   public void setPoolPingEnabled(boolean poolPingEnabled) {
235     this.poolPingEnabled = poolPingEnabled;
236     forceCloseAll();
237   }
238 
239   /**
240    * If a connection has not been used in this many milliseconds, ping the
241    * database to make sure the connection is still good.
242    *
243    * @param milliseconds the number of milliseconds of inactivity that will trigger a ping
244    */
245   public void setPoolPingConnectionsNotUsedFor(int milliseconds) {
246     this.poolPingConnectionsNotUsedFor = milliseconds;
247     forceCloseAll();
248   }
249 
250   public String getDriver() {
251     return dataSource.getDriver();
252   }
253 
254   public String getUrl() {
255     return dataSource.getUrl();
256   }
257 
258   public String getUsername() {
259     return dataSource.getUsername();
260   }
261 
262   public String getPassword() {
263     return dataSource.getPassword();
264   }
265 
266   public boolean isAutoCommit() {
267     return dataSource.isAutoCommit();
268   }
269 
270   public Integer getDefaultTransactionIsolationLevel() {
271     return dataSource.getDefaultTransactionIsolationLevel();
272   }
273 
274   public Properties getDriverProperties() {
275     return dataSource.getDriverProperties();
276   }
277 
278   /**
279    * @since 3.5.2
280    */
281   public Integer getDefaultNetworkTimeout() {
282     return dataSource.getDefaultNetworkTimeout();
283   }
284 
285   public int getPoolMaximumActiveConnections() {
286     return poolMaximumActiveConnections;
287   }
288 
289   public int getPoolMaximumIdleConnections() {
290     return poolMaximumIdleConnections;
291   }
292 
293   public int getPoolMaximumLocalBadConnectionTolerance() {
294     return poolMaximumLocalBadConnectionTolerance;
295   }
296 
297   public int getPoolMaximumCheckoutTime() {
298     return poolMaximumCheckoutTime;
299   }
300 
301   public int getPoolTimeToWait() {
302     return poolTimeToWait;
303   }
304 
305   public String getPoolPingQuery() {
306     return poolPingQuery;
307   }
308 
309   public boolean isPoolPingEnabled() {
310     return poolPingEnabled;
311   }
312 
313   public int getPoolPingConnectionsNotUsedFor() {
314     return poolPingConnectionsNotUsedFor;
315   }
316 
317   /**
318    * Closes all active and idle connections in the pool.
319    */
320   public void forceCloseAll() {
321     synchronized (state) {
322       expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
323       for (int i = state.activeConnections.size(); i > 0; i--) {
324         try {
325           PooledConnection conn = state.activeConnections.remove(i - 1);
326           conn.invalidate();
327 
328           Connection realConn = conn.getRealConnection();
329           if (!realConn.getAutoCommit()) {
330             realConn.rollback();
331           }
332           realConn.close();
333         } catch (Exception e) {
334           // ignore
335         }
336       }
337       for (int i = state.idleConnections.size(); i > 0; i--) {
338         try {
339           PooledConnection conn = state.idleConnections.remove(i - 1);
340           conn.invalidate();
341 
342           Connection realConn = conn.getRealConnection();
343           if (!realConn.getAutoCommit()) {
344             realConn.rollback();
345           }
346           realConn.close();
347         } catch (Exception e) {
348           // ignore
349         }
350       }
351     }
352     if (log.isDebugEnabled()) {
353       log.debug("PooledDataSource forcefully closed/removed all connections.");
354     }
355   }
356 
357   public PoolState getPoolState() {
358     return state;
359   }
360 
361   private int assembleConnectionTypeCode(String url, String username, String password) {
362     return ("" + url + username + password).hashCode();
363   }
364 
365   protected void pushConnection(PooledConnection conn) throws SQLException {
366 
367     synchronized (state) {
368       state.activeConnections.remove(conn);
369       if (conn.isValid()) {
370         if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
371           state.accumulatedCheckoutTime += conn.getCheckoutTime();
372           if (!conn.getRealConnection().getAutoCommit()) {
373             conn.getRealConnection().rollback();
374           }
375           PooledConnectionPooledConnection.html#PooledConnection">PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
376           state.idleConnections.add(newConn);
377           newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
378           newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
379           conn.invalidate();
380           if (log.isDebugEnabled()) {
381             log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
382           }
383           state.notifyAll();
384         } else {
385           state.accumulatedCheckoutTime += conn.getCheckoutTime();
386           if (!conn.getRealConnection().getAutoCommit()) {
387             conn.getRealConnection().rollback();
388           }
389           conn.getRealConnection().close();
390           if (log.isDebugEnabled()) {
391             log.debug("Closed connection " + conn.getRealHashCode() + ".");
392           }
393           conn.invalidate();
394         }
395       } else {
396         if (log.isDebugEnabled()) {
397           log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
398         }
399         state.badConnectionCount++;
400       }
401     }
402   }
403 
404   private PooledConnection popConnection(String username, String password) throws SQLException {
405     boolean countedWait = false;
406     PooledConnection conn = null;
407     long t = System.currentTimeMillis();
408     int localBadConnectionCount = 0;
409 
410     while (conn == null) {
411       synchronized (state) {
412         if (!state.idleConnections.isEmpty()) {
413           // Pool has available connection
414           conn = state.idleConnections.remove(0);
415           if (log.isDebugEnabled()) {
416             log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
417           }
418         } else {
419           // Pool does not have available connection
420           if (state.activeConnections.size() < poolMaximumActiveConnections) {
421             // Can create new connection
422             conn = new PooledConnection(dataSource.getConnection(), this);
423             if (log.isDebugEnabled()) {
424               log.debug("Created connection " + conn.getRealHashCode() + ".");
425             }
426           } else {
427             // Cannot create new connection
428             PooledConnection oldestActiveConnection = state.activeConnections.get(0);
429             long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
430             if (longestCheckoutTime > poolMaximumCheckoutTime) {
431               // Can claim overdue connection
432               state.claimedOverdueConnectionCount++;
433               state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
434               state.accumulatedCheckoutTime += longestCheckoutTime;
435               state.activeConnections.remove(oldestActiveConnection);
436               if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
437                 try {
438                   oldestActiveConnection.getRealConnection().rollback();
439                 } catch (SQLException e) {
440                   /*
441                      Just log a message for debug and continue to execute the following
442                      statement like nothing happened.
443                      Wrap the bad connection with a new PooledConnection, this will help
444                      to not interrupt current executing thread and give current thread a
445                      chance to join the next competition for another valid/good database
446                      connection. At the end of this loop, bad {@link @conn} will be set as null.
447                    */
448                   log.debug("Bad connection. Could not roll back");
449                 }
450               }
451               conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
452               conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
453               conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
454               oldestActiveConnection.invalidate();
455               if (log.isDebugEnabled()) {
456                 log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
457               }
458             } else {
459               // Must wait
460               try {
461                 if (!countedWait) {
462                   state.hadToWaitCount++;
463                   countedWait = true;
464                 }
465                 if (log.isDebugEnabled()) {
466                   log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
467                 }
468                 long wt = System.currentTimeMillis();
469                 state.wait(poolTimeToWait);
470                 state.accumulatedWaitTime += System.currentTimeMillis() - wt;
471               } catch (InterruptedException e) {
472                 break;
473               }
474             }
475           }
476         }
477         if (conn != null) {
478           // ping to server and check the connection is valid or not
479           if (conn.isValid()) {
480             if (!conn.getRealConnection().getAutoCommit()) {
481               conn.getRealConnection().rollback();
482             }
483             conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
484             conn.setCheckoutTimestamp(System.currentTimeMillis());
485             conn.setLastUsedTimestamp(System.currentTimeMillis());
486             state.activeConnections.add(conn);
487             state.requestCount++;
488             state.accumulatedRequestTime += System.currentTimeMillis() - t;
489           } else {
490             if (log.isDebugEnabled()) {
491               log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
492             }
493             state.badConnectionCount++;
494             localBadConnectionCount++;
495             conn = null;
496             if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
497               if (log.isDebugEnabled()) {
498                 log.debug("PooledDataSource: Could not get a good connection to the database.");
499               }
500               throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
501             }
502           }
503         }
504       }
505 
506     }
507 
508     if (conn == null) {
509       if (log.isDebugEnabled()) {
510         log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
511       }
512       throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
513     }
514 
515     return conn;
516   }
517 
518   /**
519    * Method to check to see if a connection is still usable
520    *
521    * @param conn - the connection to check
522    * @return True if the connection is still usable
523    */
524   protected boolean pingConnection(PooledConnection conn) {
525     boolean result = true;
526 
527     try {
528       result = !conn.getRealConnection().isClosed();
529     } catch (SQLException e) {
530       if (log.isDebugEnabled()) {
531         log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
532       }
533       result = false;
534     }
535 
536     if (result) {
537       if (poolPingEnabled) {
538         if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
539           try {
540             if (log.isDebugEnabled()) {
541               log.debug("Testing connection " + conn.getRealHashCode() + " ...");
542             }
543             Connection realConn = conn.getRealConnection();
544             try (Statement statement = realConn.createStatement()) {
545               statement.executeQuery(poolPingQuery).close();
546             }
547             if (!realConn.getAutoCommit()) {
548               realConn.rollback();
549             }
550             result = true;
551             if (log.isDebugEnabled()) {
552               log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
553             }
554           } catch (Exception e) {
555             log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
556             try {
557               conn.getRealConnection().close();
558             } catch (Exception e2) {
559               //ignore
560             }
561             result = false;
562             if (log.isDebugEnabled()) {
563               log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
564             }
565           }
566         }
567       }
568     }
569     return result;
570   }
571 
572   /**
573    * Unwraps a pooled connection to get to the 'real' connection
574    *
575    * @param conn - the pooled connection to unwrap
576    * @return The 'real' connection
577    */
578   public static Connection unwrapConnection(Connection conn) {
579     if (Proxy.isProxyClass(conn.getClass())) {
580       InvocationHandler handler = Proxy.getInvocationHandler(conn);
581       if (handler instanceof PooledConnection) {
582         return ((PooledConnection) handler).getRealConnection();
583       }
584     }
585     return conn;
586   }
587 
588   @Override
589   protected void finalize() throws Throwable {
590     forceCloseAll();
591     super.finalize();
592   }
593 
594   @Override
595   public <T> T unwrap(Class<T> iface) throws SQLException {
596     throw new SQLException(getClass().getName() + " is not a wrapper.");
597   }
598 
599   @Override
600   public boolean isWrapperFor(Class<?> iface) {
601     return false;
602   }
603 
604   @Override
605   public Logger getParentLogger() {
606     return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
607   }
608 
609 }