001package com.hfg.sql.jdbc; 002 003import java.sql.Connection; 004import java.sql.DatabaseMetaData; 005import java.sql.SQLException; 006import java.util.ArrayList; 007import java.util.Date; 008import java.util.HashSet; 009import java.util.List; 010import java.util.Set; 011import java.util.logging.Level; 012import java.util.logging.Logger; 013 014import com.hfg.datetime.DateUtil; 015import com.hfg.security.LoginCredentials; 016import com.hfg.sql.SQLUtil; 017import com.hfg.util.collection.CollectionUtil; 018import com.hfg.util.StringBuilderPlus; 019import com.hfg.util.StringUtil; 020 021//------------------------------------------------------------------------------ 022/** 023 A JDBC-compatible connection pool. 024 <div> 025 @author J. Alex Taylor, hairyfatguy.com 026 </div> 027 */ 028//------------------------------------------------------------------------------ 029// com.hfg XML/HTML Coding Library 030// 031// This library is free software; you can redistribute it and/or 032// modify it under the terms of the GNU Lesser General Public 033// License as published by the Free Software Foundation; either 034// version 2.1 of the License, or (at your option) any later version. 035// 036// This library is distributed in the hope that it will be useful, 037// but WITHOUT ANY WARRANTY; without even the implied warranty of 038// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 039// Lesser General Public License for more details. 040// 041// You should have received a copy of the GNU Lesser General Public 042// License along with this library; if not, write to the Free Software 043// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 044// 045// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 046// jataylor@hairyfatguy.com 047//------------------------------------------------------------------------------ 048 049public abstract class JDBCConnectionPool<T extends JDBCConnectionPoolSettings> extends JDBCDataSource<T> 050{ 051 private List<PooledJDBCConnection> mPooledConnections; 052 private Thread mIdleConnReaperThread; 053 private int mNumConnectionRequests = 0; 054 private boolean mIsActive = true; 055 private Set<String> mBorrowerClassesToIgnore; 056 057 private Integer mDatabaseMajorVersion; 058 private Integer mDatabaseMinorVersion; 059 private String mDatabaseVersionString; 060 061 private final static Logger LOGGER = Logger.getLogger(JDBCConnectionPool.class.getName()); 062 063 static 064 { 065 LOGGER.setLevel(Level.INFO); 066 } 067 068 //########################################################################### 069 // CONSTRUCTORS 070 //########################################################################### 071 072 //--------------------------------------------------------------------------- 073 public JDBCConnectionPool(JDBCServer inServer, String inDatabaseName) 074 { 075 super(inServer, inDatabaseName); 076 } 077 078 //--------------------------------------------------------------------------- 079 public JDBCConnectionPool(JDBCServer inServer, String inDatabaseName, LoginCredentials inCredentials) 080 { 081 super(inServer, inDatabaseName); 082 setCredentials(inCredentials); 083 } 084 085 086 //########################################################################### 087 // PUBLIC METHODS 088 //########################################################################### 089 090 //--------------------------------------------------------------------------- 091 public synchronized JDBCConnection getConnection() 092 throws SQLException, NoAvailableConnectionException 093 { 094 JDBCConnection conn = null; 095 096 long startTime = System.currentTimeMillis(); 097 098 StackTraceElement borrower = getBorrower(); 099 100 while (null == conn) 101 { 102 try 103 { 104 conn = getPoolConnection(borrower); 105 } 106 catch (NoAvailableConnectionException e) 107 { 108 if (getSettings().getAvailabilityTimeoutSec() != null 109 && System.currentTimeMillis() - startTime > (getSettings().getAvailabilityTimeoutSec() * 1000)) 110 { 111 throw e; 112 } 113 else 114 { 115 try 116 { 117 Thread.sleep(250); 118 } 119 catch (InterruptedException e2) 120 { 121 // Ignore 122 } 123 } 124 } 125 } 126 127 mNumConnectionRequests++; 128 129 return conn; 130 } 131 132 //--------------------------------------------------------------------------- 133 public synchronized void destroy() 134 throws SQLException 135 { 136 mIsActive = false; 137 138 if (CollectionUtil.hasValues(mPooledConnections)) 139 { 140 for (PooledJDBCConnection conn : mPooledConnections) 141 { 142 conn.destroy(); 143 } 144 145 mPooledConnections.clear(); 146 } 147 148 mIdleConnReaperThread.interrupt(); 149 } 150 151 //--------------------------------------------------------------------------- 152 /** 153 Specifies a class to be ignored when tracking connection lending. When examining 154 the stacktrace to see who is requesting a connection, any elements from this class 155 will be skipped and a subsequent element will be used to identify the borrower. 156 * @param inValue a class to be ignored when tracking connection lending 157 * @return this connection pool object to enable method chaining 158 */ 159 public JDBCConnectionPool addIgnorableBorrowerClass(Class inValue) 160 { 161 if (inValue != null) 162 { 163 if (null == mBorrowerClassesToIgnore) 164 { 165 mBorrowerClassesToIgnore = new HashSet<>(5); 166 } 167 168 mBorrowerClassesToIgnore.add(inValue.getName()); 169 } 170 171 return this; 172 } 173 174 //--------------------------------------------------------------------------- 175 public int getDatabaseMajorVersion() 176 throws SQLException 177 { 178 if (null == mDatabaseMajorVersion) 179 { 180 Connection conn = null; 181 try 182 { 183 conn = getConnection(); 184 185 DatabaseMetaData meta = conn.getMetaData(); 186 // Oracle (and some other vendors) do not support 187 // some the following methods; therefore, we need 188 // to use try-catch block. 189 try 190 { 191 mDatabaseMajorVersion = meta.getDatabaseMajorVersion(); 192 } 193 catch (Exception e) 194 { 195 System.err.println("Database major version: unsupported feature"); 196 } 197 } 198 finally 199 { 200 SQLUtil.close(conn); 201 } 202 203 } 204 205 return mDatabaseMajorVersion; 206 } 207 208 //--------------------------------------------------------------------------- 209 public int getDatabaseMinorVersion() 210 throws SQLException 211 { 212 if (null == mDatabaseMinorVersion) 213 { 214 Connection conn = null; 215 try 216 { 217 conn = getConnection(); 218 219 DatabaseMetaData meta = conn.getMetaData(); 220 // Oracle (and some other vendors) do not support 221 // some the following methods; therefore, we need 222 // to use try-catch block. 223 try 224 { 225 mDatabaseMinorVersion = meta.getDatabaseMinorVersion(); 226 } 227 catch (Exception e) 228 { 229 System.err.println("Database minor version: unsupported feature"); 230 } 231 } 232 finally 233 { 234 SQLUtil.close(conn); 235 } 236 237 } 238 239 return mDatabaseMajorVersion; 240 } 241 242 //--------------------------------------------------------------------------- 243 public String getDatabaseVersionString() 244 { 245 if (null == mDatabaseVersionString) 246 { 247 Connection conn = null; 248 try 249 { 250 conn = getConnection(); 251 252 DatabaseMetaData meta = conn.getMetaData(); 253 mDatabaseVersionString = meta.getDatabaseProductVersion(); 254 } 255 catch (SQLException e) 256 { 257 throw new JDBCException("Error retrieving database version string!", e); 258 } 259 finally 260 { 261 SQLUtil.close(conn); 262 } 263 264 } 265 266 return mDatabaseVersionString; 267 } 268 269 //--------------------------------------------------------------------------- 270 public String getStatusReport() 271 { 272 StringBuilderPlus buffer = new StringBuilderPlus() 273 .appendln("Connection Pool Status Report " + DateUtil.getYYYY_MM_DD_HH_mm_ss(new Date())) 274 .appendln() 275 .appendln(String.format(" %s %s", getServer().getRDBMS().name(), getDatabaseVersionString())) 276 .appendln(" Server: " + getServer().getConnectString(getDatabaseName())) 277 .appendln(" Num Connection Requests: " + mNumConnectionRequests) 278 .appendln(String.format(" Connections (Min/Max/Avail.): %d / %d / %d", 279 getSettings().getMinConnections(), getSettings().getMaxConnections(), getNumAvailableConnections())); 280 281 StringBuilderPlus activeConnBuffer = new StringBuilderPlus(); 282 if (CollectionUtil.hasValues(mPooledConnections)) 283 { 284 for (int i = 0; i < mPooledConnections.size(); i++) 285 { 286 PooledJDBCConnection poolConn = mPooledConnections.get(i); 287 288 if (! poolConn.isAvailable()) 289 { 290 activeConnBuffer.appendln(String.format(" %2d. %s [%s]", (i + 1), poolConn.getBorrower().toString(), DateUtil.generateElapsedTimeString(poolConn.getCheckoutTime()))); 291 } 292 } 293 } 294 295 if (activeConnBuffer.length() > 0) 296 { 297 buffer.appendln(" Active Connections:") 298 .append(activeConnBuffer); 299 } 300 else 301 { 302 buffer.appendln(" No Active Connections."); 303 } 304 305 return buffer.toString(); 306 } 307 308 //--------------------------------------------------------------------------- 309 public static Logger getLogger() 310 { 311 return LOGGER; 312 } 313 314 //########################################################################### 315 // PROTECTED METHODS 316 //########################################################################### 317 318 //--------------------------------------------------------------------------- 319 protected JDBCConnection getPoolConnection(StackTraceElement inBorrower) 320 throws SQLException, NoAvailableConnectionException 321 { 322 PooledJDBCConnection conn = null; 323 324 if (null == mPooledConnections) 325 { 326 initializePool(); 327 } 328 329 for (int i = 0; i < mPooledConnections.size(); i++) 330 { 331 PooledJDBCConnection poolConn = mPooledConnections.get(i); 332 333 if (poolConn.isAvailable()) 334 { 335 try 336 { 337 conn = poolConn.checkout(inBorrower); 338 if (null == conn) 339 { // The connection must not be valid anymore 340 poolConn.destroy(); 341 mPooledConnections.remove(i--); 342 } 343 else 344 { 345 // Connection is good to go 346 break; 347 } 348 } 349 catch (Exception e) 350 { 351 e.printStackTrace(); 352 poolConn.destroy(); 353 mPooledConnections.remove(i--); 354 } 355 } 356 } 357 358 if (null == conn) 359 { 360 // No available connections were found in the pool. Can we create a new one? 361 if (mPooledConnections.size() < getSettings().getMaxConnections()) 362 { 363 conn = new PooledJDBCConnection(createConnection()); 364 LOGGER.fine("Creating new connection: " + conn.name()); 365 conn = conn.checkout(inBorrower); 366 if (conn != null) 367 { 368 mPooledConnections.add(conn); 369 } 370 } 371 else 372 { 373 throw new NoAvailableConnectionException(); 374 } 375 } 376 377 return conn; 378 } 379 380 //--------------------------------------------------------------------------- 381 protected T initSettings() 382 { 383 return (T) new JDBCConnectionPoolSettings(); 384 } 385 386 //--------------------------------------------------------------------------- 387 protected abstract JDBCConnection createConnection(); 388 389 //########################################################################### 390 // PRIVATE METHODS 391 //########################################################################### 392 393 //--------------------------------------------------------------------------- 394 private void initializePool() 395 { 396 mPooledConnections = new ArrayList<>(getSettings().getMaxConnections()); 397 398 for (int i = 0; i < getSettings().getMinConnections(); i++) 399 { 400 JDBCConnection conn = createConnection(); 401 402 mPooledConnections.add(new PooledJDBCConnection(conn)); 403 } 404 405 mIdleConnReaperThread = new Thread(new IdleConnReaper()); 406 mIdleConnReaperThread.setDaemon(true); 407 mIdleConnReaperThread.setPriority(Thread.MIN_PRIORITY); 408 mIdleConnReaperThread.setName(generateIdleRepearThreadName()); 409 mIdleConnReaperThread.start(); 410 } 411 412 413 //--------------------------------------------------------------------------- 414 private String generateIdleRepearThreadName() 415 { 416 StringBuilderPlus name = new StringBuilderPlus().setDelimiter(" "); 417 if (StringUtil.isSet(name())) 418 { 419 name.append(name()); 420 } 421 422 if (StringUtil.isSet(getServer().name())) 423 { 424 name.delimitedAppend(getServer().name()); 425 } 426 427 if (StringUtil.isSet(getDatabaseName())) 428 { 429 name.delimitedAppend(getDatabaseName()); 430 } 431 432 name.delimitedAppend(" Idle Conn Reaper"); 433 434 return name.toString(); 435 } 436 437 //--------------------------------------------------------------------------- 438 private StackTraceElement getBorrower() 439 { 440 StackTraceElement borrower = null; 441 442 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 443 for (int i = 3; i < stackTrace.length; i++) 444 { 445 StackTraceElement stackTraceElement = stackTrace[i]; 446 if (! CollectionUtil.hasValues(mBorrowerClassesToIgnore) 447 || ! mBorrowerClassesToIgnore.contains(stackTraceElement.getClassName())) 448 { 449 borrower = stackTraceElement; 450 break; 451 } 452 } 453 454 return borrower; 455 } 456 457 //--------------------------------------------------------------------------- 458 private synchronized void removeConnectionFromPool(PooledJDBCConnection inConn) 459 { 460 mPooledConnections.remove(inConn); 461 } 462 463 //--------------------------------------------------------------------------- 464 private synchronized void removeIdledConnections() 465 throws SQLException 466 { 467 if (poolContainsMoreThanTheMinNumOfConnections()) 468 { 469 for (int i = 0; i < getSettings().getMinConnections(); i++) 470 { 471 PooledJDBCConnection poolConn = mPooledConnections.get(i); 472 if (poolConn.isAvailable() 473 && poolConn.getIdleTimeSec() > getSettings().getMaxIdleTimeSec()) 474 { 475 poolConn.destroy(); 476 mPooledConnections.remove(i--); 477 478 if (! poolContainsMoreThanTheMinNumOfConnections()) 479 { 480 break; 481 } 482 } 483 } 484 } 485 } 486 487 //--------------------------------------------------------------------------- 488 private boolean poolContainsMoreThanTheMinNumOfConnections() 489 { 490 return (mPooledConnections.size() > 0 491 && (null == getSettings().getMinConnections() 492 || mPooledConnections.size() > getSettings().getMinConnections())); 493 } 494 495 //--------------------------------------------------------------------------- 496 private int getNumAvailableConnections() 497 { 498 int numAvailableConns = 0; 499 500 if (mPooledConnections != null) 501 { 502 for (int i = 0; i < mPooledConnections.size(); i++) 503 { 504 PooledJDBCConnection poolConn = mPooledConnections.get(i); 505 506 if (poolConn.isAvailable()) 507 { 508 numAvailableConns++; 509 } 510 } 511 } 512 513 return numAvailableConns; 514 } 515 516 //########################################################################### 517 // INNER CLASS 518 //########################################################################### 519 520 private class PooledJDBCConnection extends JDBCConnection 521 { 522 private boolean mIsAvailable; 523 private Long mCheckoutTime; 524 private StackTraceElement mBorrower; 525 private Long mIdleStartTime; 526 527 //------------------------------------------------------------------------ 528 public PooledJDBCConnection(JDBCConnection inConn) 529 { 530 super(inConn); 531 initPooledConnection(); 532 } 533 534 //------------------------------------------------------------------------ 535 private void initPooledConnection() 536 { 537 mCheckoutTime = null; 538 mBorrower = null; 539 mIdleStartTime = System.currentTimeMillis(); 540 mIsAvailable = true; 541 } 542 543 //------------------------------------------------------------------------ 544 public synchronized boolean isAvailable() 545 { 546 return mIsAvailable; 547 } 548 549 //------------------------------------------------------------------------ 550 public synchronized PooledJDBCConnection checkout(StackTraceElement inBorrower) 551 throws SQLException 552 { 553 mIsAvailable = false; 554 mCheckoutTime = System.currentTimeMillis(); 555 mBorrower = inBorrower; 556 mIdleStartTime = null; 557 558 if (! getAutoCommit()) 559 { 560 setAutoCommit(true); 561 } 562 563 PooledJDBCConnection conn = this; 564 565 if (getServer().getRDBMS().getConnTestQuery() != null) 566 { 567 try 568 { 569 execute(getServer().getRDBMS().getConnTestQuery().toSQL()); 570 } 571 catch (JDBCException e) 572 { 573 conn = null; 574 } 575 } 576 577 if (conn != null) 578 { 579 LOGGER.fine(name() + " checked out to " + inBorrower); 580 } 581 582 return conn; 583 } 584 585 //------------------------------------------------------------------------ 586 public Long getCheckoutTime() 587 { 588 return mCheckoutTime; 589 } 590 591 //------------------------------------------------------------------------ 592 public StackTraceElement getBorrower() 593 { 594 return mBorrower; 595 } 596 597 //------------------------------------------------------------------------ 598 private int getIdleTimeSec() 599 { 600 int idleTimeSec = 0; 601 if (mIdleStartTime != null) 602 { 603 idleTimeSec = (int) (System.currentTimeMillis() - mIdleStartTime)/1000; 604 } 605 606 return idleTimeSec; 607 } 608 609 //------------------------------------------------------------------------ 610 // Don't actually close the underlying connection. It goes back into the pool. 611 public void close() 612 throws SQLException 613 { 614 LOGGER.fine(name() + " returned from " + mBorrower); 615 616 if (isClosed()) 617 { 618 removeConnectionFromPool(this); 619 } 620 else 621 { 622 clean(); 623 624 initPooledConnection(); 625 } 626 } 627 628 //------------------------------------------------------------------------ 629 public void destroy() 630 throws SQLException 631 { 632 if (! isClosed()) 633 { 634 unwrap(Connection.class).close(); 635 } 636 } 637 638 //------------------------------------------------------------------------ 639 private void clean() 640 throws SQLException 641 { 642 if (! getAutoCommit()) 643 { 644 boolean returnedInUncommittedState = false; 645 try 646 { 647 setAutoCommit(true); 648 } 649 catch (SQLException e) 650 { 651 returnedInUncommittedState = true; 652 653 // Try rolling back the transaction. 654 try 655 { 656 rollback(); 657 setAutoCommit(true); 658 } 659 catch (SQLException e2) 660 { 661 destroy(); 662 removeConnectionFromPool(this); 663 } 664 } 665 666 if (returnedInUncommittedState) 667 { 668 throw new JDBCException("Connection returned in an uncommitted state!"); 669 } 670 } 671 } 672 673 674 //------------------------------------------------------------------------ 675 @Override 676 public String getCurrentUser() 677 throws SQLException 678 { 679 return ((JDBCConnection) getInnerConnection()).getCurrentUser(); 680 } 681 682 } 683 684 //########################################################################### 685 // INNER CLASS 686 //########################################################################### 687 688 private class IdleConnReaper implements Runnable 689 { 690 private int mExecutionFreqSec = 60; 691 692 //------------------------------------------------------------------------ 693 public void run() 694 { 695 while (mIsActive) 696 { 697 try 698 { 699 Thread.sleep(mExecutionFreqSec * 1000); 700 } 701 catch (InterruptedException e2) 702 { 703 // Ignore 704 } 705 706 try 707 { 708 removeIdledConnections(); 709 } 710 catch (Exception e) 711 { 712 // Ignore 713 e.printStackTrace(); 714 } 715 } 716 } 717 718 } 719}