/* * File : PooledConnectionBeanProvider.java * Created : 21-oct-2003 20:13 * By : fbusquets * * JClic - Authoring and playing system for educational activities * * Copyright (C) 2000 - 2005 Francesc Busquets & Departament * d'Educacio de la Generalitat de Catalunya * * Based on DbConnectionBeanBroker. * version 1.0.13 3/12/02 * by Marc A. Mnich * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details (see the LICENSE file). */ package edu.xtec.util.db; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; /** Implementation of {@link ConnectionBeanProvider} that uses a pool of * {@link edu.xtec.util.db.ConnectionBean} objects. * Based on DbConnectionBroker 1.0.13, by Marc A. Mnich * @author fbusquets * @version 1.0.13 */ public class PooledConnectionBeanProvider extends ConnectionBeanProvider implements Runnable{ // Additional parameter names /** Key for minimum number of connections to start with. Default is 1. */ public static final String MIN_CONNS = "dbMinConns"; private static final String DEFAULT_MIN_CONNS = "1"; /** Key for maximum number of connections in dynamic pool. Default is 3 */ public static final String MAX_CONNS = "dbMaxConns"; private static final String DEFAULT_MAX_CONNS = "3"; /** Key for absolute path name for log file. e.g. 'c:/temp/mylog.log'. Default is 'pooledConnectionBean.log' in user's dir */ public static final String LOG_FILE="dbLogFile"; private static final String DEFAULT_LOG_FILE="connectionPool.log"; /** Key for time in days between connection resets. (Reset does a basic cleanup). Default is 1.0 */ public static final String MAX_CONN_TIME="dbMaxConnDays"; private static final String DEFAULT_MAX_CONN_TIME="1.0"; /** Key for append to logfile. Default is true.*/ public static final String LOG_APPEND="dbLogAppend"; private static final String DEFAULT_LOG_APPEND="true"; /** Key for max time a connection can be checked out before being recycled. Zero value turns option off. Default is 60.*/ public static final String MAX_CHECKOUT_SECONDS="dbMaxCheckoutSeconds"; private static final String DEFAULT_MAX_CHECKOUT_SECONDS="60"; /** Key for level of debug messages output to the log file. 0 -> no messages, 1 -> Errors, 2 -> Warnings, 3 -> Information. Default is 2. */ public static final String DEBUG_LEVEL="dbDebugLevel"; private static final String DEFAULT_DEBUG_LEVEL="2"; private Thread runner; private ConnectionBean[] connPool; private int[] connStatus; private long[] connLockTime, connCreateDate; private String[] connID; private String logFileString, logPIDFileString; private int currConnections, connLast, minConns, maxConns, maxConnMSec, maxCheckoutSeconds, debugLevel; //available: set to false on destroy, checked by getConnection() private boolean available=true; private PrintWriter log; private SQLWarning currSQLWarning; private String pid; /** * Number of times this ConnectionBean provider has been used since its creation. */ public int globalUsageCount; /** Main initialization function, called immediatelly after constructor by * getConnectionBeanProvider functions. * @param map Collection of key - value pairs that must specify the driver, url, login and * password of the just created ConnectionBeanProvider. * @throws Exception Throwed if dbDriver does not contain a valid driver name, or if it can't be * instantiated. */ protected void setUp(Map map) throws Exception { super.setUp(map); if(dbDriver==null || dbDriver.length()==0) throw new Exception("Parameter dbDriver is null!"); Class.forName(dbDriver); if(dbServer==null || dbServer.length()==0) throw new Exception("Parameter dbServer is null!"); minConns = Math.max(1, Integer.parseInt(getValue(map, MIN_CONNS, DEFAULT_MIN_CONNS))); maxConns = Math.max(minConns, Math.min(15, Integer.parseInt(getValue(map, MAX_CONNS, DEFAULT_MAX_CONNS)))); logFileString = getValue(map, LOG_FILE, DEFAULT_LOG_FILE); double maxConnTime = new Double(getValue(map, MAX_CONN_TIME, DEFAULT_MAX_CONN_TIME)).doubleValue(); boolean logAppend = new Boolean(getValue(map, LOG_APPEND, DEFAULT_LOG_APPEND)).booleanValue(); maxCheckoutSeconds = Integer.parseInt(getValue(map, MAX_CHECKOUT_SECONDS, DEFAULT_MAX_CHECKOUT_SECONDS)); debugLevel = Integer.parseInt(getValue(map, DEBUG_LEVEL, DEFAULT_DEBUG_LEVEL)); connPool = new ConnectionBean[maxConns]; connStatus = new int[maxConns]; connLockTime = new long[maxConns]; connCreateDate = new long[maxConns]; connID = new String[maxConns]; currConnections = minConns; File f=new File(logFileString); if(!f.isAbsolute()){ f=new File(System.getProperty("user.home")); f=new File(f, logFileString); logFileString=f.getAbsolutePath(); } logPIDFileString=logFileString+".pid"; maxConnMSec = (int)(maxConnTime * 86400000.0); //86400 sec/day if(maxConnMSec < 30000) { // Recycle no less than 30 seconds. maxConnMSec = 30000; } if(debugLevel>0){ try { log = new PrintWriter(new FileOutputStream(logFileString, logAppend),true); // Can't open the requested file. Open the default file. } catch (IOException e1) { try { log = new PrintWriter(new FileOutputStream("DBConn_" + System.currentTimeMillis() + ".log", logAppend),true); } catch (IOException e2) { throw new IOException("Can't open any log file"); } } } // Write the pid file (used to clean up dead/broken connection) SimpleDateFormat formatter = new SimpleDateFormat("yyyy.MM.dd G 'at' hh:mm:ss a zzz"); Date nowc = new Date(); pid = formatter.format(nowc); BufferedWriter pidout = new BufferedWriter(new FileWriter(logPIDFileString)); pidout.write(pid); pidout.close(); if(log!=null){ log.println("-----------------------------------------"); log.println(new Date()); log.println("Starting DbConnectionBeanBroker Version 1.0.13:"); log.println("dbDriver = " + dbDriver); log.println("dbServer = " + dbServer); log.println("dbLogin = " + dbLogin); log.println("log file = " + logFileString); log.println("minconnections = " + minConns); log.println("maxconnections = " + maxConns); log.println("Total refresh interval = " + maxConnTime + " days"); log.println("logAppend = " + logAppend); log.println("maxCheckoutSeconds = " + maxCheckoutSeconds); log.println("debugLevel = " + debugLevel); log.println("mapStatements = " + mapStatements); log.println("-----------------------------------------"); } // 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; Exception sqlEx=null; //int dbLoop=20; int dbLoop=3; try { for(int i=1; i < dbLoop; i++) { try { for(int j=0; j < currConnections; j++) { createConn(j); } connectionsSucceeded=true; break; } catch (SQLException e){ sqlEx=e; if(log!=null && debugLevel > 0) { StringBuffer sb=new StringBuffer(); sb.append(new Date()).append(" ->Attempt (").append(i); sb.append(" of ").append(dbLoop).append(") failed to create new connections set at startup:\n"); sb.append(e).append("\n"); sb.append("Will try again in 15 seconds..."); log.println(sb.substring(0)); } try { Thread.sleep(15000); } catch(InterruptedException e1) {} } } if(!connectionsSucceeded) { // All attempts at connecting to db exhausted if(log!=null && debugLevel > 0) { log.println("\r\nAll attempts at connecting to Database exhausted"); } if(sqlEx==null) sqlEx=new IOException("Unable to connect to Database"); throw sqlEx; } } catch (Exception e) { throw e; //throw new IOException(); } // Fire up the background housekeeping thread runner = new Thread(this); runner.start(); }//End DbConnectionBeanBroker() /** * Housekeeping thread. Runs in the background with low CPU overhead. * Connections are checked for warnings and closure and are periodically * restarted. * 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). * This method acts as fault tolerance for bad connection/statement programming. */ public void run() { boolean forever = true; Statement stmt=null; String currCatalog=null; long maxCheckoutMillis = maxCheckoutSeconds * 1000; while(forever) { /* * CHECK OF PID DISABLED * FileReader fr=null; BufferedReader in=null; // Make sure the log file is the one this instance opened // If not, clean it up! try { fr=new FileReader(logPIDFileString); in = new BufferedReader(fr); String curr_pid = in.readLine(); if(curr_pid.equals(pid)) { //log.println("They match = " + curr_pid); } else { //log.println("No match = " + curr_pid); if(log!=null) log.close(); // Close all connections silently - they are definitely dead. for(int i=0; i < currConnections; i++) { try { connPool[i].closeConnection(); } catch (SQLException e1) {} // ignore } // Returning from the run() method kills the thread return; } // moved to "finally" //in.close(); } catch (IOException e1) { if(log!=null){ log.print(new Date().toString() + "Can't read the file for pid info: " + logPIDFileString + " - "); log.println(e1.getMessage()); } } finally{ try{ if(in!=null) in.close(); if(fr!=null) fr.close(); in=null; fr=null; } catch(IOException ex){ // ignore exceptions on close } } */ // Get any Warnings on connections and print to event file for(int i=0; i < currConnections; i++) { try { currSQLWarning = connPool[i].getConnection().getWarnings(); if(currSQLWarning != null) { if(log!=null && debugLevel > 1) { log.println(new Date().toString()+" - Warnings on connection " + String.valueOf(i) + " " + currSQLWarning); } connPool[i].getConnection().clearWarnings(); } } catch(SQLException e) { if(log!=null && debugLevel > 1) { log.println("Cannot access 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]; if(log!=null && debugLevel > 2) { log.println(new Date().toString()+" - Warning. Connection " + i + " in use for " + timeInUse + " ms"); } if(maxCheckoutMillis != 0) { if(timeInUse > maxCheckoutMillis) { if(log!=null && debugLevel > 1) { log.println(new Date().toString()+" Warning. 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].getConnection().createStatement(); connStatus[i] = 0; // Connection is O.K. //log.println("Connection confirmed for conn = " + // String.valueOf(i)); // Some DBs return an object even if DB is shut down if(connPool[i].getConnection().isClosed()) { throw new SQLException(); } // Connection has a problem, restart it } catch(SQLException e) { if(log!=null && debugLevel > 1) { log.println(new Date().toString() + " ***** Recycling connection " + String.valueOf(i) + ":"); } try { connPool[i].closeConnection(); } catch(SQLException e0) { if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" - Error! Can't close connection! Might have been closed already. Trying to recycle anyway... (" + e0 + ")"); } } try { createConn(i); } catch(SQLException e1) { if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" - Failed to create connection: " + e1); } connStatus[i] = 0; // Can't open, try again next time } } finally { try{ if(stmt != null) { stmt.close(); } } catch(SQLException e1){ }; } } try { Thread.sleep(20000); // 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. * * 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 The ConnectionBean object, ready to be used. Remember to free it using * freeConnectionBean, as explained in {@link * ConnectionBeanProvider#freeConnectionBean} ConnectionBeanProvider. */ public ConnectionBean getConnectionBean() { ConnectionBean 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].getConnection().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==false)&&(loop < currConnections)); } catch (SQLException e1) { if(log!=null) log.println(new Date().toString()+" - Error: " + e1); } if(gotOne) { break; } else { synchronized(this) { // Add new connections to the pool if(currConnections < maxConns) { try { createConn(currConnections); currConnections++; } catch(SQLException e) { if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" - Error: Unable to create new connection: " + e); } } } } try { Thread.sleep(2000); } catch(InterruptedException e) { } if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" --> Connections Exhausted! Will wait and try again in loop " + String.valueOf(outerloop)); } } } // End of try 10 times loop } else { if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" - Unsuccessful getConnection() request during destroy()"); } } // End if(available) if(log!=null && debugLevel > 2) { log.println(new Date().toString()+" - Handing out connection " + idOfConnection(conn)); } if(conn!=null) conn.usageCount++; return conn; } /** Returns the local JDBC ID for a connection. * @param conn The ConnectionBean owner if the Connection. * @return The JDBC ID. */ public int idOfConnection(ConnectionBean conn) { int match; String tag; try { tag = conn.getConnection().toString(); } catch (NullPointerException e1) { tag = "none"; } match=-1; 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 ConnectionBean to be released. * @return A String useful only for debug purposes. */ public String freeConnectionBean(ConnectionBean conn) { StringBuffer res=new StringBuffer(); int thisconn = idOfConnection(conn); if(thisconn >= 0) { connStatus[thisconn]=0; res.append("freed ").append(conn.getConnection().toString()); //log.println("Freed connection " + String.valueOf(thisconn) + // " normal exit: "); } else { if(log!=null && debugLevel > 0) { log.println(new Date().toString()+" --> Error: Could not free connection!!!"); } } return res.substring(0); } /** Returns the age of a connection -- the time since it was handed out to * an application. * @param conn The ConnectionBean to be examined. * @return The age of the Connection, measured in milliseconds. */ public long getAge(ConnectionBean 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 { if(connPool[i]!=null){ globalUsageCount+=connPool[i].usageCount; } Date now = new Date(); try { Class.forName(dbDriver); connPool[i] = new ConnectionBean(DriverManager.getConnection (dbServer,dbLogin,dbPassword), mapStatements); connStatus[i]=0; connID[i]=connPool[i].getConnection().toString(); connLockTime[i]=0; connCreateDate[i] = now.getTime(); } catch (ClassNotFoundException e2) { if(log!=null && debugLevel > 0) { log.println(now.toString()+" - Error creating connection: " + e2); } } if(log!=null) log.println(now.toString() + " Opening connection " + String.valueOf(i) + " " + connPool[i].getConnection().toString() + ":"); } /** * Shuts down the housekeeping thread and closes all connections * in the pool. Call this method from the destroy() method of the servlet. */ /** * Multi-phase shutdown. having following sequence: *
getConnection()
will refuse to return connections.
* millis
milliseconds after shutdown of
* the housekeeping thread, freeConnection()
can still be
* called to return used connections.
* millis
milliseconds after the shutdown of the
* housekeeping thread, all connections in the pool are closed.
* SQLException
is thrown.
* millis
.
*/
public void destroy(int millis) throws SQLException {
// 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(millis); }
catch(InterruptedException e){} // 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(500); }
catch(InterruptedException e) {} // ignore
}
// Close all connections, whether safe or not
for(int i=0; i < currConnections; i++) {
try {
connPool[i].closeConnection();
} catch (SQLException e1) {
if(log!=null && debugLevel > 0) {
log.println(new Date().toString()+" - Cannot close connections on Destroy");
}
}
}
if(useCount > 0) {
//bt-test successful
String msg=new Date().toString()+" - Unsafe shutdown: Had to close "+useCount+
" active DB connections after "+millis+"ms";
if(log!=null){
log.println(msg);
// Close all open files
log.close();
}
// Throwing following Exception is essential because servlet authors
// are likely to have their own error logging requirements.
throw new SQLException(msg);
}
// Close all open files
if(log!=null)
log.close();
}//End destroy()
/**
* Less safe shutdown. Uses default timeout value.
* This method simply calls the destroy()
method
* with a millis
* value of 10000 (10 seconds) and ignores SQLException
* thrown by that method.
* @see #destroy(int)
*/
protected void destroy() {
try {
destroy(10000);
}
catch(SQLException e) {}
}
/** Returns the number of connections in use.
* @return The nomber of ConnectionBean objects in use.
*/
// 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).
public int getUseCount() {
int useCount=0;
synchronized(connStatus) {
for(int i=0; i < currConnections; i++) {
if(connStatus[i] > 0) { // In use
useCount++;
}
}
}
return useCount;
}
/** Returns the number of connections in the dynamic pool.
* @return The number of ConnectionBean objects created.
*/
public int getSize() {
return currConnections;
}
/** Provides information about the current state of this ConnectionBeanProvider.
* @return Information string, formatted in HTML.
*/
public String getInfo(){
int totalUsageCount=globalUsageCount;
StringBuffer sb=new StringBuffer();
sb.append("PooledConnectionBeanProvider ").append(hashCode()).append("