View Javadoc
1   /*
2    *    Copyright 2009-2022 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 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
168    *          The maximum number of active connections
169    */
170   public void setPoolMaximumActiveConnections(int poolMaximumActiveConnections) {
171     this.poolMaximumActiveConnections = poolMaximumActiveConnections;
172     forceCloseAll();
173   }
174 
175   /**
176    * The maximum number of idle connections.
177    *
178    * @param poolMaximumIdleConnections
179    *          The maximum number of idle connections
180    */
181   public void setPoolMaximumIdleConnections(int poolMaximumIdleConnections) {
182     this.poolMaximumIdleConnections = poolMaximumIdleConnections;
183     forceCloseAll();
184   }
185 
186   /**
187    * The maximum number of tolerance for bad connection happens in one thread
188    * which are applying for new {@link PooledConnection}.
189    *
190    * @param poolMaximumLocalBadConnectionTolerance
191    *          max tolerance for bad connection happens in one thread
192    *
193    * @since 3.4.5
194    */
195   public void setPoolMaximumLocalBadConnectionTolerance(
196       int poolMaximumLocalBadConnectionTolerance) {
197     this.poolMaximumLocalBadConnectionTolerance = poolMaximumLocalBadConnectionTolerance;
198   }
199 
200   /**
201    * The maximum time a connection can be used before it *may* be
202    * given away again.
203    *
204    * @param poolMaximumCheckoutTime
205    *          The maximum time
206    */
207   public void setPoolMaximumCheckoutTime(int poolMaximumCheckoutTime) {
208     this.poolMaximumCheckoutTime = poolMaximumCheckoutTime;
209     forceCloseAll();
210   }
211 
212   /**
213    * The time to wait before retrying to get a connection.
214    *
215    * @param poolTimeToWait
216    *          The time to wait
217    */
218   public void setPoolTimeToWait(int poolTimeToWait) {
219     this.poolTimeToWait = poolTimeToWait;
220     forceCloseAll();
221   }
222 
223   /**
224    * The query to be used to check a connection.
225    *
226    * @param poolPingQuery
227    *          The query
228    */
229   public void setPoolPingQuery(String poolPingQuery) {
230     this.poolPingQuery = poolPingQuery;
231     forceCloseAll();
232   }
233 
234   /**
235    * Determines if the ping query should be used.
236    *
237    * @param poolPingEnabled
238    *          True if we need to check a connection before using it
239    */
240   public void setPoolPingEnabled(boolean poolPingEnabled) {
241     this.poolPingEnabled = poolPingEnabled;
242     forceCloseAll();
243   }
244 
245   /**
246    * If a connection has not been used in this many milliseconds, ping the
247    * database to make sure the connection is still good.
248    *
249    * @param milliseconds
250    *          the number of milliseconds of inactivity that will trigger a ping
251    */
252   public void setPoolPingConnectionsNotUsedFor(int milliseconds) {
253     this.poolPingConnectionsNotUsedFor = milliseconds;
254     forceCloseAll();
255   }
256 
257   public String getDriver() {
258     return dataSource.getDriver();
259   }
260 
261   public String getUrl() {
262     return dataSource.getUrl();
263   }
264 
265   public String getUsername() {
266     return dataSource.getUsername();
267   }
268 
269   public String getPassword() {
270     return dataSource.getPassword();
271   }
272 
273   public boolean isAutoCommit() {
274     return dataSource.isAutoCommit();
275   }
276 
277   public Integer getDefaultTransactionIsolationLevel() {
278     return dataSource.getDefaultTransactionIsolationLevel();
279   }
280 
281   public Properties getDriverProperties() {
282     return dataSource.getDriverProperties();
283   }
284 
285   /**
286    * Gets the default network timeout.
287    *
288    * @return the default network timeout
289    * @since 3.5.2
290    */
291   public Integer getDefaultNetworkTimeout() {
292     return dataSource.getDefaultNetworkTimeout();
293   }
294 
295   public int getPoolMaximumActiveConnections() {
296     return poolMaximumActiveConnections;
297   }
298 
299   public int getPoolMaximumIdleConnections() {
300     return poolMaximumIdleConnections;
301   }
302 
303   public int getPoolMaximumLocalBadConnectionTolerance() {
304     return poolMaximumLocalBadConnectionTolerance;
305   }
306 
307   public int getPoolMaximumCheckoutTime() {
308     return poolMaximumCheckoutTime;
309   }
310 
311   public int getPoolTimeToWait() {
312     return poolTimeToWait;
313   }
314 
315   public String getPoolPingQuery() {
316     return poolPingQuery;
317   }
318 
319   public boolean isPoolPingEnabled() {
320     return poolPingEnabled;
321   }
322 
323   public int getPoolPingConnectionsNotUsedFor() {
324     return poolPingConnectionsNotUsedFor;
325   }
326 
327   /**
328    * Closes all active and idle connections in the pool.
329    */
330   public void forceCloseAll() {
331     synchronized (state) {
332       expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword());
333       for (int i = state.activeConnections.size(); i > 0; i--) {
334         try {
335           PooledConnection conn = state.activeConnections.remove(i - 1);
336           conn.invalidate();
337 
338           Connection realConn = conn.getRealConnection();
339           if (!realConn.getAutoCommit()) {
340             realConn.rollback();
341           }
342           realConn.close();
343         } catch (Exception e) {
344           // ignore
345         }
346       }
347       for (int i = state.idleConnections.size(); i > 0; i--) {
348         try {
349           PooledConnection conn = state.idleConnections.remove(i - 1);
350           conn.invalidate();
351 
352           Connection realConn = conn.getRealConnection();
353           if (!realConn.getAutoCommit()) {
354             realConn.rollback();
355           }
356           realConn.close();
357         } catch (Exception e) {
358           // ignore
359         }
360       }
361     }
362     if (log.isDebugEnabled()) {
363       log.debug("PooledDataSource forcefully closed/removed all connections.");
364     }
365   }
366 
367   public PoolState getPoolState() {
368     return state;
369   }
370 
371   private int assembleConnectionTypeCode(String url, String username, String password) {
372     return ("" + url + username + password).hashCode();
373   }
374 
375   protected void pushConnection(PooledConnection conn) throws SQLException {
376 
377     synchronized (state) {
378       state.activeConnections.remove(conn);
379       if (conn.isValid()) {
380         if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) {
381           state.accumulatedCheckoutTime += conn.getCheckoutTime();
382           if (!conn.getRealConnection().getAutoCommit()) {
383             conn.getRealConnection().rollback();
384           }
385           PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this);
386           state.idleConnections.add(newConn);
387           newConn.setCreatedTimestamp(conn.getCreatedTimestamp());
388           newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp());
389           conn.invalidate();
390           if (log.isDebugEnabled()) {
391             log.debug("Returned connection " + newConn.getRealHashCode() + " to pool.");
392           }
393           state.notifyAll();
394         } else {
395           state.accumulatedCheckoutTime += conn.getCheckoutTime();
396           if (!conn.getRealConnection().getAutoCommit()) {
397             conn.getRealConnection().rollback();
398           }
399           conn.getRealConnection().close();
400           if (log.isDebugEnabled()) {
401             log.debug("Closed connection " + conn.getRealHashCode() + ".");
402           }
403           conn.invalidate();
404         }
405       } else {
406         if (log.isDebugEnabled()) {
407           log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection.");
408         }
409         state.badConnectionCount++;
410       }
411     }
412   }
413 
414   private PooledConnection popConnection(String username, String password) throws SQLException {
415     boolean countedWait = false;
416     PooledConnection conn = null;
417     long t = System.currentTimeMillis();
418     int localBadConnectionCount = 0;
419 
420     while (conn == null) {
421       synchronized (state) {
422         if (!state.idleConnections.isEmpty()) {
423           // Pool has available connection
424           conn = state.idleConnections.remove(0);
425           if (log.isDebugEnabled()) {
426             log.debug("Checked out connection " + conn.getRealHashCode() + " from pool.");
427           }
428         } else {
429           // Pool does not have available connection
430           if (state.activeConnections.size() < poolMaximumActiveConnections) {
431             // Can create new connection
432             conn = new PooledConnection(dataSource.getConnection(), this);
433             if (log.isDebugEnabled()) {
434               log.debug("Created connection " + conn.getRealHashCode() + ".");
435             }
436           } else {
437             // Cannot create new connection
438             PooledConnection oldestActiveConnection = state.activeConnections.get(0);
439             long longestCheckoutTime = oldestActiveConnection.getCheckoutTime();
440             if (longestCheckoutTime > poolMaximumCheckoutTime) {
441               // Can claim overdue connection
442               state.claimedOverdueConnectionCount++;
443               state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime;
444               state.accumulatedCheckoutTime += longestCheckoutTime;
445               state.activeConnections.remove(oldestActiveConnection);
446               if (!oldestActiveConnection.getRealConnection().getAutoCommit()) {
447                 try {
448                   oldestActiveConnection.getRealConnection().rollback();
449                 } catch (SQLException e) {
450                   /*
451                      Just log a message for debug and continue to execute the following
452                      statement like nothing happened.
453                      Wrap the bad connection with a new PooledConnection, this will help
454                      to not interrupt current executing thread and give current thread a
455                      chance to join the next competition for another valid/good database
456                      connection. At the end of this loop, bad {@link @conn} will be set as null.
457                    */
458                   log.debug("Bad connection. Could not roll back");
459                 }
460               }
461               conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this);
462               conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp());
463               conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp());
464               oldestActiveConnection.invalidate();
465               if (log.isDebugEnabled()) {
466                 log.debug("Claimed overdue connection " + conn.getRealHashCode() + ".");
467               }
468             } else {
469               // Must wait
470               try {
471                 if (!countedWait) {
472                   state.hadToWaitCount++;
473                   countedWait = true;
474                 }
475                 if (log.isDebugEnabled()) {
476                   log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection.");
477                 }
478                 long wt = System.currentTimeMillis();
479                 state.wait(poolTimeToWait);
480                 state.accumulatedWaitTime += System.currentTimeMillis() - wt;
481               } catch (InterruptedException e) {
482                 // set interrupt flag
483                 Thread.currentThread().interrupt();
484                 break;
485               }
486             }
487           }
488         }
489         if (conn != null) {
490           // ping to server and check the connection is valid or not
491           if (conn.isValid()) {
492             if (!conn.getRealConnection().getAutoCommit()) {
493               conn.getRealConnection().rollback();
494             }
495             conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password));
496             conn.setCheckoutTimestamp(System.currentTimeMillis());
497             conn.setLastUsedTimestamp(System.currentTimeMillis());
498             state.activeConnections.add(conn);
499             state.requestCount++;
500             state.accumulatedRequestTime += System.currentTimeMillis() - t;
501           } else {
502             if (log.isDebugEnabled()) {
503               log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection.");
504             }
505             state.badConnectionCount++;
506             localBadConnectionCount++;
507             conn = null;
508             if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) {
509               if (log.isDebugEnabled()) {
510                 log.debug("PooledDataSource: Could not get a good connection to the database.");
511               }
512               throw new SQLException("PooledDataSource: Could not get a good connection to the database.");
513             }
514           }
515         }
516       }
517 
518     }
519 
520     if (conn == null) {
521       if (log.isDebugEnabled()) {
522         log.debug("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
523       }
524       throw new SQLException("PooledDataSource: Unknown severe error condition.  The connection pool returned a null connection.");
525     }
526 
527     return conn;
528   }
529 
530   /**
531    * Method to check to see if a connection is still usable
532    *
533    * @param conn
534    *          - the connection to check
535    * @return True if the connection is still usable
536    */
537   protected boolean pingConnection(PooledConnection conn) {
538     boolean result = true;
539 
540     try {
541       result = !conn.getRealConnection().isClosed();
542     } catch (SQLException e) {
543       if (log.isDebugEnabled()) {
544         log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
545       }
546       result = false;
547     }
548 
549     if (result && poolPingEnabled && poolPingConnectionsNotUsedFor >= 0
550         && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) {
551       try {
552         if (log.isDebugEnabled()) {
553           log.debug("Testing connection " + conn.getRealHashCode() + " ...");
554         }
555         Connection realConn = conn.getRealConnection();
556         try (Statement statement = realConn.createStatement()) {
557           statement.executeQuery(poolPingQuery).close();
558         }
559         if (!realConn.getAutoCommit()) {
560           realConn.rollback();
561         }
562         result = true;
563         if (log.isDebugEnabled()) {
564           log.debug("Connection " + conn.getRealHashCode() + " is GOOD!");
565         }
566       } catch (Exception e) {
567         log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage());
568         try {
569           conn.getRealConnection().close();
570         } catch (Exception e2) {
571           // ignore
572         }
573         result = false;
574         if (log.isDebugEnabled()) {
575           log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage());
576         }
577       }
578     }
579     return result;
580   }
581 
582   /**
583    * Unwraps a pooled connection to get to the 'real' connection
584    *
585    * @param conn
586    *          - the pooled connection to unwrap
587    * @return The 'real' connection
588    */
589   public static Connection unwrapConnection(Connection conn) {
590     if (Proxy.isProxyClass(conn.getClass())) {
591       InvocationHandler handler = Proxy.getInvocationHandler(conn);
592       if (handler instanceof PooledConnection) {
593         return ((PooledConnection) handler).getRealConnection();
594       }
595     }
596     return conn;
597   }
598 
599   @Override
600   protected void finalize() throws Throwable {
601     forceCloseAll();
602     super.finalize();
603   }
604 
605   @Override
606   public <T> T unwrap(Class<T> iface) throws SQLException {
607     throw new SQLException(getClass().getName() + " is not a wrapper.");
608   }
609 
610   @Override
611   public boolean isWrapperFor(Class<?> iface) {
612     return false;
613   }
614 
615   @Override
616   public Logger getParentLogger() {
617     return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
618   }
619 
620 }