View Javadoc
1   /**
2    *    Copyright 2009-2015 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.ResultSet;
24  import java.sql.SQLException;
25  import java.sql.Statement;
26  import java.util.Properties;
27  import java.util.logging.Logger;
28  
29  import javax.sql.DataSource;
30  
31  import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
32  import org.apache.ibatis.logging.Log;
33  import org.apache.ibatis.logging.LogFactory;
34  
35  /**
36   * This is a simple, synchronous, thread-safe database connection pool.
37   *
38   * @author Clinton Begin
39   */
40  public class PooledDataSource implements DataSource {
41  
42    private static final Log log = LogFactory.getLog(PooledDataSource.class);
43  
44    private final PoolState state = new PoolState(this);
45  
46    private final UnpooledDataSource dataSource;
47  
48    // OPTIONAL CONFIGURATION FIELDS
49    protected int poolMaximumActiveConnections = 10;
50    protected int poolMaximumIdleConnections = 5;
51    protected int poolMaximumCheckoutTime = 20000;
52    protected int poolTimeToWait = 20000;
53    protected String poolPingQuery = "NO PING QUERY SET";
54    protected boolean poolPingEnabled = false;
55    protected int poolPingConnectionsNotUsedFor = 0;
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) throws SQLException {
99      DriverManager.setLoginTimeout(loginTimeout);
100   }
101 
102   @Override
103   public int getLoginTimeout() throws SQLException {
104     return DriverManager.getLoginTimeout();
105   }
106 
107   @Override
108   public void setLogWriter(PrintWriter logWriter) throws SQLException {
109     DriverManager.setLogWriter(logWriter);
110   }
111 
112   @Override
113   public PrintWriter getLogWriter() throws SQLException {
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    * The maximum number of active connections
154    *
155    * @param poolMaximumActiveConnections The maximum number of active connections
156    */
157   public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
158     this.poolMaximumActiveConnections = poolMaximumActiveConnections;
159     forceCloseAll();
160   }
161 
162   /*
163    * The maximum number of idle connections
164    *
165    * @param poolMaximumIdleConnections The maximum number of idle connections
166    */
167   public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
168     this.poolMaximumIdleConnections = poolMaximumIdleConnections;
169     forceCloseAll();
170   }
171 
172   /*
173    * The maximum time a connection can be used before it *may* be
174    * given away again.
175    *
176    * @param poolMaximumCheckoutTime The maximum time
177    */
178   public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
179     this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
180     forceCloseAll();
181   }
182 
183   /*
184    * The time to wait before retrying to get a connection
185    *
186    * @param poolTimeToWait The time to wait
187    */
188   public void setPoolTimeToWait(int poolTimeToWait) {
189     this.poolTimeToWait = poolTimeToWait;
190     forceCloseAll();
191   }
192 
193   /*
194    * The query to be used to check a connection
195    *
196    * @param poolPingQuery The query
197    */
198   public void setPoolPingQuery(String poolPingQuery) {
199     this.poolPingQuery = poolPingQuery;
200     forceCloseAll();
201   }
202 
203   /*
204    * Determines if the ping query should be used.
205    *
206    * @param poolPingEnabled True if we need to check a connection before using it
207    */
208   public void setPoolPingEnabled(boolean poolPingEnabled) {
209     this.poolPingEnabled = poolPingEnabled;
210     forceCloseAll();
211   }
212 
213   /*
214    * If a connection has not been used in this many milliseconds, ping the
215    * database to make sure the connection is still good.
216    *
217    * @param milliseconds the number of milliseconds of inactivity that will trigger a ping
218    */
219   public void setPoolPingConnectionsNotUsedFor(int milliseconds) {
220     this.poolPingConnectionsNotUsedFor = milliseconds;
221     forceCloseAll();
222   }
223 
224   public String getDriver() {
225     return dataSource.getDriver();
226   }
227 
228   public String getUrl() {
229     return dataSource.getUrl();
230   }
231 
232   public String getUsername() {
233     return dataSource.getUsername();
234   }
235 
236   public String getPassword() {
237     return dataSource.getPassword();
238   }
239 
240   public boolean isAutoCommit() {
241     return dataSource.isAutoCommit();
242   }
243 
244   public Integer getDefaultTransactionIsolationLevel() {
245     return dataSource.getDefaultTransactionIsolationLevel();
246   }
247 
248   public Properties getDriverProperties() {
249     return dataSource.getDriverProperties();
250   }
251 
252   public int getPoolMaximumActiveConnections() {
253     return poolMaximumActiveConnections;
254   }
255 
256   public int getPoolMaximumIdleConnections() {
257     return poolMaximumIdleConnections;
258   }
259 
260   public int getPoolMaximumCheckoutTime() {
261     return poolMaximumCheckoutTime;
262   }
263 
264   public int getPoolTimeToWait() {
265     return poolTimeToWait;
266   }
267 
268   public String getPoolPingQuery() {
269     return poolPingQuery;
270   }
271 
272   public boolean isPoolPingEnabled() {
273     return poolPingEnabled;
274   }
275 
276   public int getPoolPingConnectionsNotUsedFor() {
277     return poolPingConnectionsNotUsedFor;
278   }
279 
280   /*
281    * Closes all active and idle connections in the pool
282    */
283   public void forceCloseAll() {
284     synchronized (state) {
285       expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
286       for (int i = state.activeConnections.size(); i > 0; i--) {
287         try {
288           PooledConnection conn = state.activeConnections.remove(i - 1);
289           conn.invalidate();
290 
291           Connection realConn = conn.getRealConnection();
292           if (!realConn.getAutoCommit()) {
293             realConn.rollback();
294           }
295           realConn.close();
296         } catch (Exception e) {
297           // ignore
298         }
299       }
300       for (int i = state.idleConnections.size(); i > 0; i--) {
301         try {
302           PooledConnection conn = state.idleConnections.remove(i - 1);
303           conn.invalidate();
304 
305           Connection realConn = conn.getRealConnection();
306           if (!realConn.getAutoCommit()) {
307             realConn.rollback();
308           }
309           realConn.close();
310         } catch (Exception e) {
311           // ignore
312         }
313       }
314     }
315     if (log.isDebugEnabled()) {
316       log.debug("PooledDataSource forcefully closed/removed all connections.");
317     }
318   }
319 
320   public PoolState getPoolState() {
321     return state;
322   }
323 
324   private int assembleConnectionTypeCode(String url, String username, String password) {
325     return ("" + url + username + password).hashCode();
326   }
327 
328   protected void pushConnection(PooledConnection conn) throws SQLException {
329 
330     synchronized (state) {
331       state.activeConnections.remove(conn);
332       if (conn.isValid()) {
333         if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
334           state.accumulatedCheckoutTime += conn.getCheckoutTime();
335           if (!conn.getRealConnection().getAutoCommit()) {
336             conn.getRealConnection().rollback();
337           }
338           PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
339           state.idleConnections.add(newConn);
340           newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
341           newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
342           conn.invalidate();
343           if (log.isDebugEnabled()) {
344             log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
345           }
346           state.notifyAll();
347         } else {
348           state.accumulatedCheckoutTime += conn.getCheckoutTime();
349           if (!conn.getRealConnection().getAutoCommit()) {
350             conn.getRealConnection().rollback();
351           }
352           conn.getRealConnection().close();
353           if (log.isDebugEnabled()) {
354             log.debug("Closed connection " + conn.getRealHashCode() + ".");
355           }
356           conn.invalidate();
357         }
358       } else {
359         if (log.isDebugEnabled()) {
360           log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
361         }
362         state.badConnectionCount++;
363       }
364     }
365   }
366 
367   private PooledConnection popConnection(String username, String password) throws SQLException {
368     boolean countedWait = false;
369     PooledConnection conn = null;
370     long t = System.currentTimeMillis();
371     int localBadConnectionCount = 0;
372 
373     while (conn == null) {
374       synchronized (state) {
375         if (!state.idleConnections.isEmpty()) {
376           // Pool has available connection
377           conn = state.idleConnections.remove(0);
378           if (log.isDebugEnabled()) {
379             log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
380           }
381         } else {
382           // Pool does not have available connection
383           if (state.activeConnections.size() < poolMaximumActiveConnections) {
384             // Can create new connection
385             conn = new PooledConnection(dataSource.getConnection(), this);
386             if (log.isDebugEnabled()) {
387               log.debug("Created connection " + conn.getRealHashCode() + ".");
388             }
389           } else {
390             // Cannot create new connection
391             PooledConnection oldestActiveConnection = state.activeConnections.get(0);
392             long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
393             if (longestCheckoutTime > poolMaximumCheckoutTime) {
394               // Can claim overdue connection
395               state.claimedOverdueConnectionCount++;
396               state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
397               state.accumulatedCheckoutTime += longestCheckoutTime;
398               state.activeConnections.remove(oldestActiveConnection);
399               if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
400                 oldestActiveConnection.getRealConnection().rollback();
401               }
402               conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
403               oldestActiveConnection.invalidate();
404               if (log.isDebugEnabled()) {
405                 log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
406               }
407             } else {
408               // Must wait
409               try {
410                 if (!countedWait) {
411                   state.hadToWaitCount++;
412                   countedWait = true;
413                 }
414                 if (log.isDebugEnabled()) {
415                   log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
416                 }
417                 long wt = System.currentTimeMillis();
418                 state.wait(poolTimeToWait);
419                 state.accumulatedWaitTime += System.currentTimeMillis() - wt;
420               } catch (InterruptedException e) {
421                 break;
422               }
423             }
424           }
425         }
426         if (conn != null) {
427           if (conn.isValid()) {
428             if (!conn.getRealConnection().getAutoCommit()) {
429               conn.getRealConnection().rollback();
430             }
431             conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
432             conn.setCheckoutTimestamp(System.currentTimeMillis());
433             conn.setLastUsedTimestamp(System.currentTimeMillis());
434             state.activeConnections.add(conn);
435             state.requestCount++;
436             state.accumulatedRequestTime += System.currentTimeMillis() - t;
437           } else {
438             if (log.isDebugEnabled()) {
439               log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
440             }
441             state.badConnectionCount++;
442             localBadConnectionCount++;
443             conn = null;
444             if (localBadConnectionCount > (poolMaximumIdleConnections + 3)) {
445               if (log.isDebugEnabled()) {
446                 log.debug("PooledDataSource: Could not get a good connection to the database.");
447               }
448               throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
449             }
450           }
451         }
452       }
453 
454     }
455 
456     if (conn == null) {
457       if (log.isDebugEnabled()) {
458         log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
459       }
460       throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
461     }
462 
463     return conn;
464   }
465 
466   /*
467    * Method to check to see if a connection is still usable
468    *
469    * @param conn - the connection to check
470    * @return True if the connection is still usable
471    */
472   protected boolean pingConnection(PooledConnection conn) {
473     boolean result = true;
474 
475     try {
476       result = !conn.getRealConnection().isClosed();
477     } catch (SQLException e) {
478       if (log.isDebugEnabled()) {
479         log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
480       }
481       result = false;
482     }
483 
484     if (result) {
485       if (poolPingEnabled) {
486         if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
487           try {
488             if (log.isDebugEnabled()) {
489               log.debug("Testing connection " + conn.getRealHashCode() + " ...");
490             }
491             Connection realConn = conn.getRealConnection();
492             Statement statement = realConn.createStatement();
493             ResultSet rs = statement.executeQuery(poolPingQuery);
494             rs.close();
495             statement.close();
496             if (!realConn.getAutoCommit()) {
497               realConn.rollback();
498             }
499             result = true;
500             if (log.isDebugEnabled()) {
501               log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
502             }
503           } catch (Exception e) {
504             log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
505             try {
506               conn.getRealConnection().close();
507             } catch (Exception e2) {
508               //ignore
509             }
510             result = false;
511             if (log.isDebugEnabled()) {
512               log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
513             }
514           }
515         }
516       }
517     }
518     return result;
519   }
520 
521   /*
522    * Unwraps a pooled connection to get to the 'real' connection
523    *
524    * @param conn - the pooled connection to unwrap
525    * @return The 'real' connection
526    */
527   public static Connection unwrapConnection(Connection conn) {
528     if (Proxy.isProxyClass(conn.getClass())) {
529       InvocationHandler handler = Proxy.getInvocationHandler(conn);
530       if (handler instanceof PooledConnection) {
531         return ((PooledConnection) handler).getRealConnection();
532       }
533     }
534     return conn;
535   }
536 
537   protected void finalize() throws Throwable {
538     forceCloseAll();
539     super.finalize();
540   }
541 
542   public <T> T unwrap(Class<T> iface) throws SQLException {
543     throw new SQLException(getClass().getName() + " is not a wrapper.");
544   }
545 
546   public boolean isWrapperFor(Class<?> iface) throws SQLException {
547     return false;
548   }
549 
550   public Logger getParentLogger() {
551     return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME); // requires JDK version 1.6
552   }
553 
554 }