Commit 7776755b authored by Daniel Henninger's avatar Daniel Henninger Committed by dhenninger

[JM-343] Migrated to proxool instead of homebrew connection pool handler. ...

[JM-343] Migrated to proxool instead of homebrew connection pool handler.  Also added /proxool to admin console to see statistics from proxool.  At some point we should wrap this in a cute openfire looking page.  No API changes necessary anywhere else in code or 3rd party database connection providers!

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@9758 b35dd754-fafc-0310-a699-88a17e54d16e
parent bbc7e885
......@@ -39,6 +39,7 @@ mysql.jar | 5.0.8
objenesis | 1.0 (JMock 2.1.0)
pack200task.jar | August 5, 2004
postgres.jar | 8.2-506.jdbc3
proxool.jar | 0.9.0RC3
rome.jar | 0.9
rome-fetcher.jar | 0.9
servlet.jar | Jetty 6.1.0 (2.5)
......
......@@ -9,6 +9,7 @@
<webroots>
<root url="file://$MODULE_DIR$/../../src/web" relative="/" />
</webroots>
<sourceRoots />
<building>
<setting name="EXPLODED_URL" value="file://" />
<setting name="EXPLODED_ENABLED" value="false" />
......@@ -347,6 +348,15 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../lib/merge/proxool.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntryProperties />
</component>
</module>
......
......@@ -499,6 +499,15 @@
<SOURCES />
</library>
</orderEntry>
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/../lib/merge/proxool.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES />
</library>
</orderEntry>
<orderEntryProperties />
</component>
<component name="VcsManagerConfiguration">
......
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2007 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.database;
import org.jivesoftware.util.ClassUtils;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Database connection pool.
*
* @author Jive Software
*/
public class ConnectionPool implements Runnable {
private String driver;
private String serverURL;
private String username;
private String password;
private int minCon;
private int maxCon;
private int conTimeout;
private boolean mysqlUseUnicode;
private Thread houseKeeper;
private boolean shutdownStarted = false;
private int conCount = 0;
private int waitingForCon = 0;
private Connection[] cons;
private ConnectionWrapper[] wrappers;
private Object waitLock = new Object();
private Object conCountLock = new Object();
private AtomicInteger used = new AtomicInteger(0);
public ConnectionPool(String driver, String serverURL, String username,
String password, int minCon, int maxCon,
double conTimeout, boolean mysqlUseUnicode) throws IOException {
this.driver = driver;
this.serverURL = serverURL;
this.username = username;
this.password = password;
this.minCon = minCon;
this.maxCon = maxCon;
// Setting the timeout to 3 hours
this.conTimeout = (int)(conTimeout * 1000 * 60 * 60 * 3); // convert to milliseconds
this.mysqlUseUnicode = mysqlUseUnicode;
if (driver == null) {
Log.error("JDBC driver value is null.");
}
try {
ClassUtils.forName(driver);
DriverManager.getDriver(serverURL);
}
catch (ClassNotFoundException e) {
Log.error("Could not load JDBC driver class: " + driver);
}
catch (SQLException e) {
Log.error("Error starting connection pool.", e);
}
// Setup pool, open minimum number of connections
wrappers = new ConnectionWrapper[maxCon];
cons = new Connection[maxCon];
boolean success = false;
int maxTry = 3;
for (int i = 0; i < maxTry; i++) {
try {
for (int j = 0; j < minCon; j++) {
createCon(j);
conCount++;
}
success = true;
break;
}
catch (SQLException e) {
// close any open connections
for (int j = 0; j < minCon; j++) {
if (cons[j] != null) {
try {
cons[j].close();
cons[j] = null;
wrappers[j] = null;
conCount--;
}
catch (SQLException e1) { /* ignore */
}
}
}
// let admin know that there was a problem
Log.error("Failed to create new connections on startup. " +
"Attempt " + i + " of " + maxTry, e);
try {
Thread.sleep(10000);
}
catch (InterruptedException e1) { /* ignore */
}
}
}
if (!success) {
throw new IOException();
}
// Start the background housekeeping thread
houseKeeper = new Thread(this);
houseKeeper.setDaemon(true);
houseKeeper.start();
}
public Connection getConnection() throws SQLException {
// if we're shutting down, don't create any connections
if (shutdownStarted) {
return null;
}
// Check to see if there are any connections available. If not, then enter wait-based
// retry loop
ConnectionWrapper wrapper = getCon();
if (wrapper != null) {
synchronized (wrapper) {
wrapper.checkedout = true;
wrapper.lockTime = System.currentTimeMillis();
}
return wrapper;
}
else {
synchronized (waitLock) {
try {
waitingForCon++;
while (true) {
wrapper = getCon();
if (wrapper != null) {
--waitingForCon;
synchronized (wrapper) {
wrapper.checkedout = true;
wrapper.lockTime = System.currentTimeMillis();
}
return wrapper;
}
else {
waitLock.wait();
}
}
}
catch (InterruptedException ex) {
--waitingForCon;
waitLock.notifyAll();
throw new SQLException("Interrupted while waiting for connection to " +
"become available.");
}
}
}
}
public void freeConnection() {
used.decrementAndGet();
synchronized (waitLock) {
if (waitingForCon > 0) {
waitLock.notifyAll();
}
}
}
public void destroy() throws SQLException {
// set shutdown flag
shutdownStarted = true;
// shut down the background housekeeping thread
houseKeeper.interrupt();
// wait 1/2 second for housekeeper to die
try {
houseKeeper.join(500);
}
catch (InterruptedException e) { /* ignore */
}
// check to see if there's any currently open connections to close
for (int i = 0; i < conCount; i++) {
ConnectionWrapper wrapper = wrappers[i];
// null means that the connection hasn't been initialized, which will only occur
// if the current index is greater than the current connection count
if (wrapper == null) {
break;
}
// if it's currently checked out, wait 1/2 second then close it anyways
if (wrapper.checkedout) {
try {
Thread.sleep(500);
}
catch (InterruptedException e) {/* ignore */
}
if (wrapper.checkedout) {
Log.info("Forcefully closing connection " + i);
}
}
cons[i].close();
cons[i] = null;
wrappers[i] = null;
}
}
public int getSize() {
return conCount;
}
/**
* Housekeeping thread. This thread runs every 30 seconds and checks connections for the
* following conditions:<BR>
* <p/>
* <ul>
* <li>Connection has been open too long - it'll be closed and another connection created.
* <li>Connection hasn't been used for 30 seconds and the number of open connections is
* greater than the minimum number of connections. The connection will be closed. This
* is done so that the pool can shrink back to the minimum number of connections if the
* pool isn't being used extensively.
* <li>Unable to create a statement with the connection - it'll be reset.
* </ul>
*/
public void run() {
while (true) {
// print warnings on connections
for (int i = 0; i < maxCon; i++) {
if (cons[i] == null) {
continue;
}
try {
SQLWarning warning = cons[i].getWarnings();
if (warning != null) {
Log.warn("Connection " + i + " had warnings: " + warning);
cons[i].clearWarnings();
}
}
catch (SQLException e) {
Log.warn("Unable to get warning for connection: ", e);
}
}
int lastOpen = -1;
// go over every connection, check it's health
for (int i = maxCon - 1; i >= 0; i--) {
if (wrappers[i] == null) {
continue;
}
try {
long time = System.currentTimeMillis();
synchronized (wrappers[i]) {
if (wrappers[i].checkedout) {
if (lastOpen < i) {
lastOpen = i;
}
// if the jive property "database.defaultProvider.checkOpenConnections"
// is true check open connections to make sure they haven't been open
// for more than XX seconds (600 by default)
if ("true".equals(JiveGlobals.getXMLProperty("database.defaultProvider.checkOpenConnections"))
&& !wrappers[i].hasLoggedException)
{
int timeout = 600;
try { timeout = Integer.parseInt(JiveGlobals.getXMLProperty("database.defaultProvider.openConnectionTimeLimit")); }
catch (Exception e) { /* ignore */ }
if (time - wrappers[i].lockTime > timeout * 1000) {
wrappers[i].hasLoggedException = true;
Log.warn("Connection has been held open for too long: ",
wrappers[i].exception);
}
}
continue;
}
wrappers[i].checkedout = true;
}
// test health of connection
Statement stmt = null;
try {
stmt = cons[i].createStatement();
}
finally {
if (stmt != null) {
stmt.close();
}
}
// Can never tell
if (cons[i].isClosed()) {
throw new SQLException();
}
// check the age of the connection
if (time - wrappers[i].createTime > conTimeout) {
throw new SQLException();
}
// check to see if it's the last connection and if it's been idle for
// more than 60 secounds
if ((time - wrappers[i].checkinTime > 60 * 1000) && i > minCon &&
lastOpen <= i) {
synchronized (conCountLock) {
cons[i].close();
wrappers[i] = null;
cons[i] = null;
conCount--;
}
}
// Flag the last open connection
lastOpen = i;
// Unlock the connection
if (wrappers[i] != null) {
wrappers[i].checkedout = false;
}
}
catch (SQLException e) {
try {
synchronized (conCountLock) {
cons[i].close();
wrappers[i] = createCon(i);
// unlock the connection
wrappers[i].checkedout = false;
}
}
catch (SQLException sqle) {
Log.warn("Failed to reopen connection", sqle);
synchronized (conCountLock) {
wrappers[i] = null;
cons[i] = null;
conCount--;
}
}
}
}
try {
Thread.sleep(30 * 1000);
}
catch (InterruptedException e) {
return;
}
}
}
private synchronized ConnectionWrapper getCon() throws SQLException {
// check to see if there's a connection already available
for (int i = 0; i < conCount; i++) {
ConnectionWrapper wrapper = wrappers[i];
// null means that the connection hasn't been initialized, which will only occur
// if the current index is greater than the current connection count
if (wrapper == null) {
break;
}
synchronized (wrapper) {
if (!wrapper.checkedout) {
wrapper.setConnection(cons[i]);
wrapper.checkedout = true;
wrapper.lockTime = System.currentTimeMillis();
if ("true".equals(JiveGlobals.getXMLProperty("database.defaultProvider.checkOpenConnections"))) {
wrapper.exception = new Exception();
wrapper.hasLoggedException = false;
}
used.incrementAndGet();
return wrapper;
}
}
}
// won't create more than maxConnections
synchronized (conCountLock) {
if (conCount >= maxCon) {
return null;
}
ConnectionWrapper con = createCon(conCount);
conCount++;
used.incrementAndGet();
return con;
}
}
/**
* Create a connection, wrap it and add it to the array of open wrappers
*/
private ConnectionWrapper createCon(int index) throws SQLException {
try {
Connection con = null;
ClassUtils.forName(driver);
if (mysqlUseUnicode) {
Properties props = new Properties();
props.put("characterEncoding", "UTF-8");
props.put("useUnicode", "true");
if (username != null) {
props.put("user", username);
}
if (password != null) {
props.put("password", password);
}
con = DriverManager.getConnection(serverURL, props);
}
else {
con = DriverManager.getConnection(serverURL, username, password);
}
if (con == null) {
throw new SQLException("Unable to retrieve connection from DriverManager");
}
try {
con.setAutoCommit(true);
}
catch (SQLException e) {/* ignored */
}
// A few people have been having problems because the default transaction
// isolation level on databases is too high. READ_COMMITTED is a good
// value for everyone to use because it provides the minimum amount of
// locking that Jive needs to work well.
try {
// Supports transactions?
if (con.getMetaData().supportsTransactions()) {
con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
}
}
catch (SQLException e) {
// Ignore errors. A few databases don't support setting the transaction
// isolation level, but ignoring the error shouldn't cause problems.
}
// create the wrapper object and mark it as checked out
ConnectionWrapper wrapper = new ConnectionWrapper(con, this);
if ("true".equals(JiveGlobals.getXMLProperty("database.defaultProvider.checkOpenConnections"))) {
wrapper.exception = new Exception();
}
synchronized (conCountLock) {
cons[index] = con;
wrappers[index] = wrapper;
}
return wrapper;
}
catch (ClassNotFoundException e) {
Log.error(e);
throw new SQLException(e.getMessage());
}
}
public String toString() {
return minCon + "," + maxCon + "," + conCount + "," + used.intValue();
}
}
\ No newline at end of file
......@@ -45,6 +45,7 @@ public interface ConnectionProvider {
* on the wrapper class will release the connection from the pool.
*
* @return a Connection object.
* @throws SQLException is an SQL error occured while retrieving the connection.
*/
public Connection getConnection() throws SQLException;
......
/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright (C) 2004 Jive Software. All rights reserved.
*
* This software is published under the terms of the GNU Public License (GPL),
* a copy of which is included in this distribution.
*/
package org.jivesoftware.database;
import java.sql.Connection;
import java.sql.SQLException;
/**
* An implementation of the Connection interface that wraps an underlying
* Connection object. It releases the connection back to a connection pool
* when Connection.close() is called.
*
* @author Jive Software
*/
public class ConnectionWrapper extends AbstractConnection {
public ConnectionPool pool;
public boolean checkedout = false;
public long createTime;
public long lockTime;
public long checkinTime;
public Exception exception;
public boolean hasLoggedException = false;
public ConnectionWrapper(Connection connection, ConnectionPool pool) {
super(connection);
this.pool = pool;
createTime = System.currentTimeMillis();
lockTime = createTime;
checkinTime = lockTime;
}
public void setConnection(Connection connection) {
super.connection = connection;
}
/**
* Instead of closing the underlying connection, we simply release
* it back into the pool.
*/
public void close() throws SQLException {
synchronized (this) {
checkedout = false;
checkinTime = System.currentTimeMillis();
}
pool.freeConnection();
// Release object references. Any further method calls on the connection will fail.
// super.connection = null;
}
public String toString() {
if (connection != null) {
return connection.toString();
}
else {
return "Jive Software Connection Wrapper";
}
}
public synchronized boolean isCheckedOut() {
return checkedout;
}
}
......@@ -14,9 +14,10 @@ package org.jivesoftware.database;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.DriverManager;
import java.util.Properties;
/**
* Default Jive connection provider, which uses an internal connection pool.<p>
......@@ -25,10 +26,10 @@ import java.sql.SQLException;
*/
public class DefaultConnectionProvider implements ConnectionProvider {
private ConnectionPool connectionPool = null;
private Properties settings;
private String driver;
private String serverURL;
private String proxoolURL;
private String username;
private String password;
private int minConnections = 3;
......@@ -46,13 +47,13 @@ public class DefaultConnectionProvider implements ConnectionProvider {
*/
private boolean mysqlUseUnicode;
private Object initLock = new Object();
/**
* Creates a new DefaultConnectionProvider.
*/
public DefaultConnectionProvider() {
loadProperties();
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
}
public boolean isPooled() {
......@@ -60,71 +61,40 @@ public class DefaultConnectionProvider implements ConnectionProvider {
}
public Connection getConnection() throws SQLException {
if (connectionPool == null) {
// block until the init has been done
synchronized (initLock) {
// if still null, something has gone wrong
if (connectionPool == null) {
Log.error("DbConnectionDefaultPool.getConnection() was " +
"called before the internal pool has been initialized or " +
"there was a problem connecting to the database.");
// Attempt to connect again if setup was already done
if ("true".equals(JiveGlobals.getXMLProperty("setup"))) {
this.restart();
if (connectionPool == null) {
Log.error("Recovery logic failed to reconnect to the database.");
return null;
}
}
else {
return null;
Connection connection = null;
try {
Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
try {
connection = DriverManager.getConnection(proxoolURL, settings);
}
catch (SQLException e) {
Log.error("DbConnectionProvider: Error while getting connection: ", e);
}
}
catch (ClassNotFoundException e) {
Log.error("DbConnectionProvider: Unable to find driver: ", e);
}
return connectionPool.getConnection();
return connection;
}
public void start() {
// acquire lock so that no connections can be returned.
synchronized (initLock) {
try {
connectionPool = new ConnectionPool(driver, serverURL, username,
password, minConnections, maxConnections, connectionTimeout,
mysqlUseUnicode);
}
catch (IOException e) {
Log.error(e);
}
}
proxoolURL = "proxool.openfire:"+getDriver()+":"+getServerURL();
settings = new Properties();
settings.setProperty("proxool.maximum-connection-count", Integer.toString(getMaxConnections()));
settings.setProperty("proxool.minimum-connection-count", Integer.toString(getMinConnections()));
settings.setProperty("proxool.maximum-connection-lifetime", Integer.toString((int)(86400000 * getConnectionTimeout())));
settings.setProperty("proxool.test-before-use", "true");
settings.setProperty("proxool.test-after-use", "true");
settings.setProperty("proxool.house-keeping-test-sql", "select 1");
settings.setProperty("user", getUsername());
settings.setProperty("password", getPassword());
}
public void restart() {
// Kill off pool.
destroy();
// Reload properties.
loadProperties();
// Start a new pool.
start();
}
public void destroy() {
if (connectionPool != null) {
try {
connectionPool.destroy();
}
catch (Exception e) {
Log.error(e);
}
}
// Release reference to connectionPool
connectionPool = null;
}
public void finalize() {
destroy();
settings = null;
}
/**
......@@ -293,7 +263,7 @@ public class DefaultConnectionProvider implements ConnectionProvider {
String maxCons = JiveGlobals.getXMLProperty("database.defaultProvider.maxConnections");
String conTimeout = JiveGlobals.getXMLProperty("database.defaultProvider.connectionTimeout");
// See if we should use Unicode under MySQL
mysqlUseUnicode = Boolean.valueOf(JiveGlobals.getXMLProperty("database.mysql.useUnicode")).booleanValue();
mysqlUseUnicode = Boolean.valueOf(JiveGlobals.getXMLProperty("database.mysql.useUnicode"));
try {
if (minCons != null) {
minConnections = Integer.parseInt(minCons);
......@@ -330,6 +300,6 @@ public class DefaultConnectionProvider implements ConnectionProvider {
}
public String toString() {
return connectionPool.toString();
return "Default Connection Provider";
}
}
......@@ -19,6 +19,8 @@ import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.DriverManager;
import java.util.Properties;
/**
* A connection provider for the embedded hsqlDB database. The database file is stored at
......@@ -30,55 +32,56 @@ import java.sql.Statement;
*/
public class EmbeddedConnectionProvider implements ConnectionProvider {
private ConnectionPool connectionPool = null;
private final Object initLock = new Object();
private Properties settings;
private String serverURL;
private String driver = "org.hsqldb.jdbcDriver";
private String proxoolURL;
public EmbeddedConnectionProvider() {
System.setProperty("org.apache.commons.logging.LogFactory", "org.jivesoftware.util.log.util.CommonsLogFactory");
}
public boolean isPooled() {
return true;
}
public Connection getConnection() throws SQLException {
if (connectionPool == null) {
// Block until the init has been done
synchronized (initLock) {
// If still null, something has gone wrong
if (connectionPool == null) {
Log.error("Error: EmbeddedConnectionProvider.getConnection() was" +
"called before the internal pool has been initialized.");
return null;
Connection connection = null;
try {
Class.forName("org.logicalcobwebs.proxool.ProxoolDriver");
try {
connection = DriverManager.getConnection(proxoolURL, settings);
}
catch (SQLException e) {
Log.error("EmbeddedConnectionProvider: Error while getting connection: ", e);
}
}
return connectionPool.getConnection();
catch (ClassNotFoundException e) {
Log.error("EmbeddedConnectionProvider: Unable to find driver: ", e);
}
return connection;
}
public void start() {
// Acquire lock so that no connections can be returned while creating the pool.
synchronized (initLock) {
try {
String driver = "org.hsqldb.jdbcDriver";
File databaseDir = new File(JiveGlobals.getHomeDirectory(), File.separator +
"embedded-db");
File databaseDir = new File(JiveGlobals.getHomeDirectory(), File.separator + "embedded-db");
// If the database doesn't exist, create it.
if (!databaseDir.exists()) {
databaseDir.mkdirs();
}
String serverURL = "jdbc:hsqldb:" + databaseDir.getCanonicalPath() +
File.separator + "openfire";
String username = "sa";
String password = "";
int minConnections = 3;
int maxConnections = 25;
double connectionTimeout = 0.5;
connectionPool = new ConnectionPool(driver, serverURL, username, password,
minConnections, maxConnections, connectionTimeout, false);
try {
serverURL = "jdbc:hsqldb:" + databaseDir.getCanonicalPath() + File.separator + "openfire";
}
catch (IOException ioe) {
Log.error("Error starting connection pool.", ioe);
}
Log.error("EmbeddedConnectionProvider: Error starting connection pool: ", ioe);
}
proxoolURL = "proxool.openfire:"+driver+":"+serverURL;
settings = new Properties();
settings.setProperty("proxool.maximum-connection-count", "25");
settings.setProperty("proxool.minimum-connection-count", "3");
settings.setProperty("proxool.maximum-connection-lifetime", Integer.toString((int)(86400000 * 0.5)));
settings.setProperty("user", "sa");
settings.setProperty("password", "");
}
public void restart() {
......@@ -89,9 +92,6 @@ public class EmbeddedConnectionProvider implements ConnectionProvider {
}
public void destroy() {
if (connectionPool == null) {
return;
}
// Shutdown the database.
Connection con = null;
try {
......@@ -107,15 +107,8 @@ public class EmbeddedConnectionProvider implements ConnectionProvider {
try { if (con != null) { con.close(); } }
catch (Exception e) { Log.error(e); }
}
// Close the connection pool.
try {
connectionPool.destroy();
}
catch (Exception e) {
Log.error(e);
}
// Release reference to connectionPool
connectionPool = null;
// Blank out the settings
settings = null;
}
public void finalize() throws Throwable {
......
......@@ -89,6 +89,11 @@
<servlet-name>dwr-invoker</servlet-name>
</filter-mapping>
<filter-mapping>
<filter-name>AuthCheck</filter-name>
<servlet-name>Proxool</servlet-name>
</filter-mapping>
<listener>
<listener-class>org.jivesoftware.openfire.XMPPContextListener</listener-class>
......@@ -116,9 +121,23 @@
<servlet-class>org.jivesoftware.openfire.container.PluginIconServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>Proxool</servlet-name>
<servlet-class>org.logicalcobwebs.proxool.admin.servlet.AdminServlet</servlet-class>
<init-param>
<param-name>output</param-name>
<param-value>full</param-value>
</init-param>
</servlet>
<!--@@JSPC-SERVLETS@@-->
<servlet-mapping>
<servlet-name>Proxool</servlet-name>
<url-pattern>/proxool</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>PluginServlet</servlet-name>
<url-pattern>/plugins/*</url-pattern>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment