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