/* * 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 Francesc Busquets (fbusquets@xtec.cat) * @version 13.09.16 */ 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. */ @Override protected void setUp(Map<String, String> 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 = Boolean.valueOf(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) { StringBuilder sb=new StringBuilder(); 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){ System.err.println(e1.getMessage()); } } } 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) { StringBuilder res=new StringBuilder(); 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: * <OL> * <LI><code>getConnection()</code> will refuse to return connections. * <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. * <LI>After <code>millis</code> milliseconds after the shutdown of the * housekeeping thread, all connections in the pool are closed. * <LI>If any connections were in use while being closed then a * <code>SQLException</code> is thrown. * <LI>The log is closed. * </OL><br> * Call this method from a servlet destroy() method. * * @param millis the time to wait in milliseconds. * @exception SQLException if connections were in use after * <code>millis</code>. */ 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 <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) */ 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. */ @Override public String getInfo(){ int totalUsageCount=globalUsageCount; StringBuilder sb=new StringBuilder(); sb.append("<b>PooledConnectionBeanProvider ").append(hashCode()).append("</b><br>\n") .append(super.getInfo()) .append("PID: ").append(pid).append("<br>\n") .append("LogFileString: ").append(logFileString).append("<br>\n") .append("currConnections: ").append(currConnections).append("<br>\n") .append("connLast: ").append(connLast).append("<br>\n") .append("minConns: ").append(minConns).append("<br>\n") .append("maxConns: ").append(maxConns).append("<br>\n") .append("maxConnMSec: ").append(maxConnMSec).append("<br>\n") .append("maxCheckoutSeconds: ").append(maxCheckoutSeconds).append("<br>\n") .append("debugLevel: ").append(debugLevel).append("<br>\n") .append("CURRENT CONNECTIONS:<br>\n") .append("<hr>\n"); for(int i=0; i<maxConns; i++){ ConnectionBean cb=connPool[i]; if(cb==null) sb.append("Empty ConnectionBean<br>\n"); else{ sb.append("Id: ").append(connID[i]).append("<br>\n") .append("Status: ").append(connStatus[i]).append("<br>\n") .append("LockTime: ").append(connLockTime[i]).append("<br>\n") .append("CreateDate: ").append(new Date(connCreateDate[i])).append("<br>\n") .append("------------\n") .append(cb.getInfo()); totalUsageCount+=cb.usageCount; } sb.append("<hr>\n"); } sb.append("TOTAL STATEMENTS USED: ").append(totalUsageCount).append("<br>\n"); return sb.substring(0); } }