Initial checkin.

This commit is contained in:
Erik C. Thauvin 2004-03-28 02:05:00 +00:00
parent 7e5f191232
commit d5b0a2f3b7
32 changed files with 4320 additions and 0 deletions

View file

@ -0,0 +1,571 @@
/*
* $Source: /zpool01/javanet/scm/svn/tmp/cvs2svn/simplepool/src/net/java/dev/simplepool/SimplePool.java,v $
* $Revision: 1.1 $
* $Date: 2004-03-28 02:04:57 $
*
* Copyright (c) 2002, Marc A. Mnich (http://www.javaexchange.com/)
* All rights reserved.
*
* Copyright (c) 2004, Russell Beattie (http://www.russellbeattie.com/)
* All rights reserved.
*
* Copyright (c) 2004, Erik C. Thauvin (http://www.thauvin.net/erik/)
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License, please see:
*
* http://www.javaexchange.com/GPL.html
*/
package net.java.dev.simplepool;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* Creates and manages a pool of database connections.
*
* @author <a href="http://www.javaexchange.com/">Marc A. Mnich</a>
* @author <a href="http://www.russellbeattie.com/">Russell Beattie</a>
* @author <a href="http://www.thauvin.net/erik/">Erik C. Thauvin</a>
* @version $Revision: 1.1 $, $Date: 2004-03-28 02:04:57 $
* @since 1.0
*/
public class SimplePool implements Runnable {
private static final Log log = LogFactory.getLog(SimplePool.class);
private Thread runner;
private Connection[] connPool;
private int[] connStatus;
private long[] connLockTime;
private long[] connCreateDate;
private String[] connID;
private String driver;
private String jdbcUrl;
private String user;
private String password;
private int currConnections;
private int connLast;
private int maxConns;
private int maxConnMSec;
private int maxCheckoutSeconds = 60;
//available: set to false on destroy, checked by getConnection()
private boolean available = true;
private SQLWarning currSQLWarning;
/**
* Creates a new Connection Pool.
*
* @param driver JDBC driver. e.g. 'oracle.jdbc.driver.OracleDriver'
* @param jdbcUrl JDBC connect string. e.g. 'jdbc:oracle:thin:@203.92.21.109:1526:orcl'
* @param user Database login name. e.g. 'Scott'
* @param password Database password. e.g. 'Tiger'
* @param minConns Minimum number of connections to start with.
* @param maxConns Maximum number of connections in dynamic pool.
* @param maxConnTime Time in days between connection resets. (Reset does a basic cleanup)
* @param maxCheckoutSeconds Max time a connection can be checked out before being recycled. Zero value turns option
* off, default is 60 seconds.
*
* @throws IOException If the pool cannot be created.
*/
public SimplePool(String driver, String jdbcUrl, String user, String password, int minConns, int maxConns,
double maxConnTime, int maxCheckoutSeconds)
throws IOException {
this.connPool = new Connection[maxConns];
this.connStatus = new int[maxConns];
this.connLockTime = new long[maxConns];
this.connCreateDate = new long[maxConns];
this.connID = new String[maxConns];
this.currConnections = minConns;
this.maxConns = maxConns;
this.driver = driver;
this.jdbcUrl = jdbcUrl;
this.user = user;
this.password = password;
this.maxCheckoutSeconds = maxCheckoutSeconds;
this.maxConnMSec = (int) (maxConnTime * 86400000.0); //86400 sec/day
if (this.maxConnMSec < 30000) { // Recycle no less than 30 seconds.
this.maxConnMSec = 30000;
}
log.info("-----------------------------------------");
log.info("Starting Connection Pool:");
log.info("driver = " + driver);
log.info("jdbcUrl = " + jdbcUrl);
log.info("user = " + user);
log.info("minconnections = " + minConns);
log.info("maxconnections = " + maxConns);
log.info("Total refresh interval = " + maxConnTime + " days");
log.info("maxCheckoutSeconds = " + maxCheckoutSeconds);
log.info("-----------------------------------------");
// Initialize the pool of connections with the mininum connections:
// Problems creating connections may be caused during reboot when the
// servlet is started before the database is ready. Handle this
// by waiting and trying again. The loop allows 5 minutes for
// db reboot.
boolean connectionsSucceeded = false;
int dbLoop = 20;
try {
for (int i = 1; i < dbLoop; i++) {
try {
for (int j = 0; j < currConnections; j++) {
createConn(j);
}
connectionsSucceeded = true;
break;
} catch (SQLException e) {
log.error("Attempt (" + String.valueOf(i) + " of " + String.valueOf(dbLoop) +
") failed to create new connections set at startup.", e);
log.warn("Will try again in 15 seconds...");
try {
Thread.sleep(15000L);
} catch (InterruptedException ignore) {
;
}
}
}
if (!connectionsSucceeded) { // All attempts at connecting to db exhausted
throw new IOException("All attempts at connecting to Database exhausted.");
}
} catch (Exception e) {
log.fatal(e.getMessage(), e);
throw new IOException(e.getMessage());
}
// Fire up the background housekeeping thread
runner = new Thread(this);
runner.start();
}
/**
* Housekeeping thread. Runs in the background with low CPU overhead. Connections are checked for warnings and
* closure and are periodically restarted.
* <p/>
* This thread is a catchall for corrupted connections and prevents the buildup of open cursors. (Open cursors
* result when the application fails to close a Statement).
* <p/>
* This method acts as fault tolerance for bad connection/statement programming.
*/
public void run() {
boolean forever = true;
Statement stmt = null;
long maxCheckoutMillis = (long) (maxCheckoutSeconds * 1000);
while (forever) {
// Get any Warnings on connections and print to event file
for (int i = 0; i < currConnections; i++) {
try {
currSQLWarning = connPool[i].getWarnings();
if (currSQLWarning != null) {
log.debug("Warnings on connection [" + String.valueOf(i) + "]: " + currSQLWarning);
connPool[i].clearWarnings();
}
} catch (SQLException e) {
log.debug("Cannot access connection [" + String.valueOf(i) + "] warnings.", e);
}
}
for (int i = 0; i < currConnections; i++) { // Do for each connection
long age = System.currentTimeMillis() - connCreateDate[i];
try { // Test the connection with createStatement call
synchronized (connStatus) {
if (connStatus[i] > 0) { // In use, catch it next time!
// Check the time it's been checked out and recycle
long timeInUse = System.currentTimeMillis() - connLockTime[i];
log.warn("Connection [" + i + "] in use for " + timeInUse + " ms.");
if (maxCheckoutMillis != 0) {
if (timeInUse > maxCheckoutMillis) {
log.warn("Connection [" + i + "] failed to be returned in time. Recycling...");
throw new SQLException();
}
}
continue;
}
connStatus[i] = 2; // Take offline (2 indicates housekeeping lock)
}
if (age > maxConnMSec) { // Force a reset at the max conn time
throw new SQLException();
}
stmt = connPool[i].createStatement();
connStatus[i] = 0; // Connection is O.K.
log.trace("Connection [" + String.valueOf(i) + "] confirmed.");
// Some DBs return an object even if DB is shut down
if (connPool[i].isClosed()) {
throw new SQLException();
}
// Connection has a problem, restart it
} catch (SQLException e) {
log.debug("Recycling connection [" + String.valueOf(i) + ']');
try {
connPool[i].close();
} catch (SQLException e0) {
log.warn("Can't close connection [" + String.valueOf(i) +
"]. Might have been closed already. Trying to recycle anyway...", e);
}
try {
createConn(i);
} catch (SQLException e1) {
log.warn("Failed to create connection [" + String.valueOf(i) + ']', e1);
connStatus[i] = 0; // Can't open, try again next time
}
} finally {
try {
if (stmt != null) {
stmt.close();
}
} catch (SQLException ignore) {
;
}
}
}
try {
Thread.sleep(20000L);
} // Wait 20 seconds for next cycle
catch (InterruptedException e) {
// Returning from the run method sets the internal
// flag referenced by Thread.isAlive() to false.
// This is required because we don't use stop() to
// shutdown this thread.
return;
}
}
} // End run
/**
* This method hands out the connections in round-robin order. This prevents a faulty connection from locking up an
* application entirely. A browser 'refresh' will get the next connection while the faulty connection is cleaned up
* by the housekeeping thread.
* <p/>
* If the min number of threads are ever exhausted, new threads are added up the the max thread count. Finally, if
* all threads are in use, this method waits 2 seconds and tries again, up to ten times. After that, it returns a
* null.
*
* @return A connection from the pool.
*/
public Connection getConnection() {
Connection conn = null;
if (available) {
boolean gotOne = false;
for (int outerloop = 1; outerloop <= 10; outerloop++) {
try {
int loop = 0;
int roundRobin = connLast + 1;
if (roundRobin >= currConnections) {
roundRobin = 0;
}
do {
synchronized (connStatus) {
if ((connStatus[roundRobin] < 1) && (!connPool[roundRobin].isClosed())) {
conn = connPool[roundRobin];
connStatus[roundRobin] = 1;
connLockTime[roundRobin] = System.currentTimeMillis();
connLast = roundRobin;
gotOne = true;
break;
} else {
loop++;
roundRobin++;
if (roundRobin >= currConnections) {
roundRobin = 0;
}
}
}
} while ((!gotOne) && (loop < currConnections));
} catch (SQLException e1) {
log.debug(e1.getMessage(), e1);
}
if (gotOne) {
break;
} else {
synchronized (this) { // Add new connections to the pool
if (currConnections < maxConns) {
try {
createConn(currConnections);
currConnections++;
} catch (SQLException e) {
log.error("Unable to create new connection.", e);
}
}
}
try {
Thread.sleep(2000L);
} catch (InterruptedException ignore) {
;
}
log.debug("Connections Exhausted. Will wait and try again in loop " + String.valueOf(outerloop));
}
} // End of try 10 times loop
} else {
log.debug("Unsuccessful getConnection() request during destroy()");
} // End if(available)
log.debug("Handing out connection [" + idOfConnection(conn) + "]: " +
(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss a")).format(new Date()));
return conn;
}
/**
* Returns the local JDBC ID for a connection.
*
* @param conn The connection object.
*
* @return The local JDBC ID for the connection.
*/
public int idOfConnection(Connection conn) {
int match = -1;
String tag;
try {
tag = conn.toString();
} catch (NullPointerException e1) {
tag = "none";
}
for (int i = 0; i < currConnections; i++) {
if (connID[i].equals(tag)) {
match = i;
break;
}
}
return match;
}
/**
* Frees a connection. Replaces connection back into the main pool for reuse.
*
* @param conn The connection object.
*
* @return A status or empty string.
*/
public String freeConnection(Connection conn) {
String res = "";
int thisconn = idOfConnection(conn);
if (thisconn >= 0) {
connStatus[thisconn] = 0;
res = "freed " + conn.toString();
log.debug("Freed connection [" + String.valueOf(thisconn) + ']');
} else {
log.error("Could not free connection [" + String.valueOf(thisconn) + ']');
}
return res;
}
/**
* Returns the age of a connection -- the time since it was handed out to an application.
*
* @param conn The connection object.
*
* @return The age of the connection.
*/
public long getAge(Connection conn) { // Returns the age of the connection in millisec.
int thisconn = idOfConnection(conn);
return System.currentTimeMillis() - connLockTime[thisconn];
}
private void createConn(int i)
throws SQLException {
Date now = new Date();
try {
Class.forName(driver);
connPool[i] = DriverManager.getConnection
(jdbcUrl, user, password);
connStatus[i] = 0;
connID[i] = connPool[i].toString();
connLockTime[i] = 0L;
connCreateDate[i] = now.getTime();
} catch (ClassNotFoundException e2) {
log.debug("Error creating connection. The driver could not be loaded.", e2);
}
log.debug("Opening connection [" + String.valueOf(i) + "]: " + connPool[i].toString());
}
/**
* Shuts down the housekeeping thread and closes all connections in the pool. Call this method from the destroy()
* method of the servlet.
* <p/>
* Multi-phase shutdown having following sequence:
* </p>
* <ol>
* <li><code>getConnection()</code> will refuse to return connections.</li>
* <li>The housekeeping thread is shut down.<br>
* Up to the time of <code>millis</code> milliseconds after shutdown of the housekeeping thread,
* <code>freeConnection()</code> can still be called to return used connections.<br>
* After <code>millis</code> milliseconds after the shutdown of the housekeeping thread, all connections in the pool
* are closed.</li>
* <li>If any connections were in use while being closed then a <code>SQLException</code> is thrown.</li>
* <li>The log is closed.</li>
* </ol>
* <p/>
* Call this method from a servlet destroy() method.
*
* @param millis the time to wait in milliseconds.
*
* @throws SQLException if connections were in use after <code>millis</code>.
*/
public void destroy(int millis) throws SQLException {
log.info("Shutting down SimplePool.");
// Checking for invalid negative arguments is not necessary,
// Thread.join() does this already in runner.join().
// Stop issuing connections
available = false;
// Shut down the background housekeeping thread
runner.interrupt();
// Wait until the housekeeping thread has died.
try {
runner.join((long) millis);
} catch (InterruptedException ignore) {
;
}
// The housekeeping thread could still be running
// (e.g. if millis is too small). This case is ignored.
// At worst, this method will throw an exception with the
// clear indication that the timeout was too short.
long startTime = System.currentTimeMillis();
// Wait for freeConnection() to return any connections
// that are still used at this time.
int useCount;
while ((useCount = getUseCount()) > 0 && System.currentTimeMillis() - startTime <= millis) {
try {
Thread.sleep(500L);
} catch (InterruptedException ignore) {
;
}
}
// Close all connections, whether safe or not
for (int i = 0; i < currConnections; i++) {
try {
connPool[i].close();
} catch (SQLException e1) {
log.debug("Cannot close connections on Destroy.");
}
}
if (useCount > 0) {
//bt-test successful
String msg = "Unsafe shutdown: Had to close " + useCount + " active DB connections after " + millis +
"ms.";
log.error(msg);
// Throwing following Exception is essential because servlet authors
// are likely to have their own error logging requirements.
throw new SQLException(msg);
}
}//End destroy()
/**
* Less safe shutdown. Uses default timeout value.
* <p/>
* This method simply calls the <code>destroy()</code> method with a <code>millis</code> value of 10000 (10 seconds)
* and ignores <code>SQLException</code> thrown by that method.
*
* @see #destroy(int)
*/
public void destroy() {
try {
destroy(10000);
} catch (SQLException e) {
;
}
}
/**
* Returns the number of connections in use.
* <p/>
* This method could be reduced to return a counter that is maintained by all methods that update connStatus.
* However, it is more efficient to do it this way because: Updating the counter would put an additional burden on
* the most frequently used methods; in comparison, this method is rarely used (although essential).
*
* @return The number of connections in use.
*/
public int getUseCount() {
int useCount = 0;
synchronized (connStatus) {
for (int i = 0; i < currConnections; i++) {
if (connStatus[i] > 0) { // In use
useCount++;
}
}
}
return useCount;
}//End getUseCount()
/**
* Returns the number of connections in the dynamic pool.
*
* @return The number of connections in the pool.
*/
public int getSize() {
return currConnections;
}//End getSize()
}// End class