/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone.jndi.resourceFactories; import java.io.PrintWriter; import java.sql.Connection; import java.sql.Driver; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import javax.sql.DataSource; import sun.reflect.generics.reflectiveObjects.NotImplementedException; import winstone.Logger; import winstone.WebAppConfiguration; import winstone.WinstoneResourceBundle; /** * Implements a JDBC 2.0 pooling datasource. This is meant to act as a wrapper * around a JDBC 1.0 driver, just providing the pool management functions. * * Supports keep alives, and check-connection-before-get options, as well * as normal reclaimable pool management options like maxIdle, maxConnections and * startConnections. Additionally it supports poll-retry on full, which means the * getConnection call will block and retry after a certain period when the pool * is maxed out (good for high load conditions). * * This class was originally drawn from the generator-runtime servlet framework and * modified to make it more JDBC-API only compliant. * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: WinstoneDataSource.java,v 1.8 2006/11/07 01:30:39 rickknowles Exp $ */ public class WinstoneDataSource implements DataSource, Runnable { public static final WinstoneResourceBundle DS_RESOURCES = new WinstoneResourceBundle("winstone.jndi.resourceFactories.LocalStrings"); private String name; private String url; private Driver driver; private Properties connectProps; private int maxIdleCount; private int maxHeldCount; private int retryCount; private int retryPeriod; private String keepAliveSQL; private int keepAlivePeriod; private boolean checkBeforeGet; private int killInactivePeriod; private List usedWrappers; private List unusedRealConnections; // sempahore private List usedRealConnections; private Thread managementThread; private int loginTimeout; private PrintWriter logWriter; /** * Main constructor. Basically just calls the init method */ public WinstoneDataSource(String name, Map args, ClassLoader loader) { this.name = name; this.usedWrappers = new ArrayList(); this.usedRealConnections = new ArrayList(); this.unusedRealConnections = new ArrayList(); this.connectProps = new Properties(); // Extract pool management properties this.keepAliveSQL = WebAppConfiguration.stringArg(args, "keepAliveSQL", ""); this.keepAlivePeriod = WebAppConfiguration.intArg(args, "keepAlivePeriod", -1); this.checkBeforeGet = WebAppConfiguration.booleanArg(args, "checkBeforeGet", !this.keepAliveSQL.equals("")); this.killInactivePeriod = WebAppConfiguration.intArg(args, "killInactivePeriod", -1); this.url = WebAppConfiguration.stringArg(args, "url", null); String driverClassName = WebAppConfiguration.stringArg(args, "driverClassName", ""); if (args.get("username") != null) this.connectProps.put("user", args.get("username")); if (args.get("password") != null) this.connectProps.put("password", args.get("password")); this.maxHeldCount = WebAppConfiguration.intArg(args, "maxConnections", 20); this.maxIdleCount = WebAppConfiguration.intArg(args, "maxIdle", 10); int startCount = WebAppConfiguration.intArg(args, "startConnections", 1); this.retryCount = WebAppConfiguration.intArg(args, "retryCount", 1); this.retryPeriod = WebAppConfiguration.intArg(args, "retryPeriod", 1000); log(Logger.FULL_DEBUG, "WinstoneDataSource.Init", this.url, null); try { synchronized (this.unusedRealConnections) { if (!driverClassName.equals("")) { Class driverClass = Class.forName(driverClassName.trim(), true, loader); this.driver = (Driver) driverClass.newInstance(); for (int n = 0; n < startCount; n++) { makeNewRealConnection(this.unusedRealConnections); } } } } catch (Throwable err) { log(Logger.ERROR, "WinstoneDataSource.ErrorInCreate", this.name, err); } // Start management thread this.managementThread = new Thread(this, "DBConnectionPool management thread"); this.managementThread.start(); } /** * Close this pool - probably because we will want to re-init the pool */ public void destroy() { if (this.managementThread != null) { this.managementThread.interrupt(); this.managementThread = null; } synchronized (this.unusedRealConnections) { killPooledConnections(this.unusedRealConnections, 0); killPooledConnections(this.usedRealConnections, 0); } this.usedRealConnections.clear(); this.unusedRealConnections.clear(); this.usedWrappers.clear(); } /** * Gets a connection with a specific username/password. These are not pooled. */ public Connection getConnection(String username, String password) throws SQLException { Properties newProps = new Properties(); newProps.put("user", username); newProps.put("password", password); Connection conn = this.driver.connect(this.url, newProps); WinstoneConnection wrapper = null; synchronized (this.unusedRealConnections) { wrapper = new WinstoneConnection(conn, this); this.usedWrappers.add(wrapper); } wrapper.setAutoCommit(true); // JFLY - quercus doesn't handle autoCommit=false return wrapper; } public Connection getConnection() throws SQLException { Connection conn = getConnection(this.retryCount); conn.setAutoCommit(true); // JFLY - quercus doesn't handle autoCommit=false return conn; } /** * Get a read-write connection - preferably from the pool, but fresh if needed */ protected Connection getConnection(int retriesAllowed) throws SQLException { Connection realConnection = null; synchronized (this.unusedRealConnections) { // If we have any spare, get it from the unused pool if (this.unusedRealConnections.size() > 0) { realConnection = (Connection) this.unusedRealConnections.get(0); this.unusedRealConnections.remove(realConnection); this.usedRealConnections.add(realConnection); log(Logger.FULL_DEBUG, "WinstoneDataSource.UsingPooled", new String[] {"" + this.usedRealConnections.size(), "" + this.unusedRealConnections.size()}, null); try { return prepareWrapper(realConnection); } catch (SQLException err) { // Leave the realConnection as non-null, so we know prepareWrapper failed } } // If we are out (and not over our limit), allocate a new one else if (this.usedRealConnections.size() < maxHeldCount) { realConnection = makeNewRealConnection(this.usedRealConnections); log(Logger.FULL_DEBUG, "WinstoneDataSource.UsingNew", new String[] {"" + this.usedRealConnections.size(), "" + this.unusedRealConnections.size()}, null); try { return prepareWrapper(realConnection); } catch (SQLException err) { // Leave the realConnection as non-null, so we know prepareWrapper failed } } } if (realConnection != null) { // prepareWrapper() must have failed, so call this method again realConnection = null; return getConnection(retriesAllowed); } else if (retriesAllowed <= 0) { // otherwise throw fail message - we've blown our limit throw new SQLException(DS_RESOURCES.getString("WinstoneDataSource.Exceeded", "" + maxHeldCount)); } else { log(Logger.FULL_DEBUG, "WinstoneDataSource.Retrying", new String[] { "" + maxHeldCount, "" + retriesAllowed, "" + retryPeriod}, null); // If we are here, it's because we need to retry for a connection try { Thread.sleep(retryPeriod); } catch (InterruptedException err) {} return getConnection(retriesAllowed - 1); } } private Connection prepareWrapper(Connection realConnection) throws SQLException { // Check before get() if (this.checkBeforeGet) { try { executeKeepAlive(realConnection); } catch (SQLException err) { // Dead connection, kill it and try again killConnection(this.usedRealConnections, realConnection); throw err; } } realConnection.setAutoCommit(true); // JFLY - quercus doesn't handle autoCommit=false WinstoneConnection wrapper = new WinstoneConnection(realConnection, this); this.usedWrappers.add(wrapper); return wrapper; } /** * Releases connections back to the pool */ void releaseConnection(WinstoneConnection wrapper, Connection realConnection) throws SQLException { synchronized (this.unusedRealConnections) { if (wrapper != null) { this.usedWrappers.remove(wrapper); } if (realConnection != null) { if (this.usedRealConnections.contains(realConnection)) { this.usedRealConnections.remove(realConnection); this.unusedRealConnections.add(realConnection); log(Logger.FULL_DEBUG, "WinstoneDataSource.Releasing", new String[] {"" + this.usedRealConnections.size(), "" + this.unusedRealConnections.size()}, null); } else { log(Logger.WARNING, "WinstoneDataSource.ReleasingUnknown", null); realConnection.close(); } } } } public int getLoginTimeout() { return this.loginTimeout; } public PrintWriter getLogWriter() { return this.logWriter; } public void setLoginTimeout(int timeout) { this.loginTimeout = timeout; } public void setLogWriter(PrintWriter writer) { this.logWriter = writer; } /** * Clean up and keep-alive thread. * Note - this needs a lot more attention to the semaphore use during keepAlive etc */ public void run() { log(Logger.DEBUG, "WinstoneDataSource.MaintenanceStart", null); int keepAliveCounter = -1; int killInactiveCounter = -1; boolean threadRunning = true; while (threadRunning) { try { long startTime = System.currentTimeMillis(); // Keep alive if the time is right if ((this.keepAlivePeriod != -1) && threadRunning) { keepAliveCounter++; if (this.keepAlivePeriod <= keepAliveCounter) { synchronized (this.unusedRealConnections) { executeKeepAliveOnUnused(); } keepAliveCounter = 0; } } if (Thread.interrupted()) { threadRunning = false; } // Kill inactive connections if the time is right if ((this.killInactivePeriod != -1) && threadRunning) { killInactiveCounter++; if (this.killInactivePeriod <= killInactiveCounter) { synchronized (this.unusedRealConnections) { killPooledConnections(this.unusedRealConnections, this.maxIdleCount); } killInactiveCounter = 0; } } if ((killInactiveCounter == 0) || (keepAliveCounter == 0)) { log(Logger.FULL_DEBUG, "WinstoneDataSource.MaintenanceTime", "" + (System.currentTimeMillis() - startTime), null); } if (Thread.interrupted()) { threadRunning = false; } else { Thread.sleep(60000); // sleep 1 minute } if (Thread.interrupted()) { threadRunning = false; } } catch (InterruptedException err) { threadRunning = false; continue; } } log(Logger.DEBUG, "WinstoneDataSource.MaintenanceFinish", null); } /** * Executes keep alive for each of the connections in the supplied pool */ protected void executeKeepAliveOnUnused() { // keep alive all unused roConns now List dead = new ArrayList(); for (Iterator i = this.unusedRealConnections.iterator(); i.hasNext();) { Connection conn = (Connection) i.next(); try { executeKeepAlive(conn); } catch (SQLException errSQL) { dead.add(conn); } } for (Iterator i = dead.iterator(); i.hasNext(); ) { killConnection(this.unusedRealConnections, (Connection) i.next()); } log(Logger.FULL_DEBUG, "WinstoneDataSource.KeepAliveFinished", "" + this.unusedRealConnections.size(), null); } protected void executeKeepAlive(Connection connection) throws SQLException { if (!this.keepAliveSQL.equals("")) { PreparedStatement qryKeepAlive = null; try { qryKeepAlive = connection.prepareStatement(keepAliveSQL); qryKeepAlive.execute(); } catch (SQLException err) { log(Logger.WARNING, "WinstoneDataSource.KeepAliveFailed", err); throw err; } finally { if (qryKeepAlive != null) { qryKeepAlive.close(); } } } } /** * This makes a new rw connection. It assumes that the synchronization has taken * place in the calling code, so is unsafe for use outside this class. */ protected Connection makeNewRealConnection(List pool) throws SQLException { if (this.url == null) { throw new SQLException("No JDBC URL supplied"); } Connection realConnection = this.driver.connect(this.url, this.connectProps); pool.add(realConnection); log(Logger.FULL_DEBUG, "WinstoneDataSource.AddingNew", new String[] {"" + this.usedRealConnections.size(), "" + this.unusedRealConnections.size()}, null); return realConnection; } /** * Iterates through a list and kills off unused connections until we reach the * minimum idle count for that pool. */ protected void killPooledConnections(List pool, int maxIdleCount) { // kill inactive unused roConns now int killed = 0; while (pool.size() > maxIdleCount) { killed++; Connection conn = (Connection) pool.get(0); killConnection(pool, conn); } if (killed > 0) { log(Logger.FULL_DEBUG, "WinstoneDataSource.Killed", "" + killed, null); } } private static void killConnection(List pool, Connection conn) { pool.remove(conn); try { conn.close(); } catch (SQLException err) { } } private void log(int level, String msgKey, Throwable err) { if (getLogWriter() != null) { getLogWriter().println(DS_RESOURCES.getString(msgKey)); if (err != null) { err.printStackTrace(getLogWriter()); } } else { Logger.log(level, DS_RESOURCES, msgKey, err); } } private void log(int level, String msgKey, String arg, Throwable err) { if (getLogWriter() != null) { getLogWriter().println(DS_RESOURCES.getString(msgKey, arg)); if (err != null) { err.printStackTrace(getLogWriter()); } } else { Logger.log(level, DS_RESOURCES, msgKey, arg, err); } } private void log(int level, String msgKey, String arg[], Throwable err) { if (getLogWriter() != null) { getLogWriter().println(DS_RESOURCES.getString(msgKey, arg)); if (err != null) { err.printStackTrace(getLogWriter()); } } else { Logger.log(level, DS_RESOURCES, msgKey, arg, err); } } public String toString() { return DS_RESOURCES.getString("WinstoneDataSource.StatusMsg", new String[] { this.name, "" + this.usedRealConnections.size(), "" + this.unusedRealConnections.size()}); } @Override public <T> T unwrap(Class<T> iface) throws SQLException { throw new NotImplementedException(); } @Override public boolean isWrapperFor(Class<?> iface) throws SQLException { throw new NotImplementedException(); } public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { throw new NotImplementedException(); } }