/*
* Copyright (C) 2000 - 2013 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD 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.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://openbd.org/
* $Id: LongTermDataSourcePoolManager.java 2327 2013-02-10 22:26:44Z alan $
*/
package com.naryx.tagfusion.cfm.sql.pool.longterm;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import javax.sql.ConnectionEvent;
import javax.sql.ConnectionEventListener;
import org.aw20.net.SocketUtil;
import org.aw20.util.SystemClock;
import org.aw20.util.SystemClockEvent;
import com.nary.util.FastMap;
import com.nary.util.string;
import com.naryx.tagfusion.cfm.engine.cfDateData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfStructData;
import com.naryx.tagfusion.cfm.sql.cfDataSourceDetails;
import com.naryx.tagfusion.cfm.sql.pool.SQLFailedConnectionException;
import com.naryx.tagfusion.cfm.sql.pool.WrappedConnection;
/*
* This class handles all the connections within a given datasource
*/
public class LongTermDataSourcePoolManager extends Object implements ConnectionEventListener, SystemClockEvent {
public static final int SOCKET_WAIT_TIME_MS = 5000;
public static final int IDLE_TIMEOUT_MS = 1000 * 60 * 2;
public static final String USER_PROP_NAME = "user";
public static final String PASSWORD_PROP_NAME = "password";
private List<WrappedConnection> idleQueue, activeQueue;
private int maxConnections, loginTimeoutSecs, usageTimeoutMs, maxUsage, maxLiveTimeMs, connectionRetries;
private String datasourceName, jdbcUri, jdbcUsername, jdbcPassword, initString;
private int statsNewCon = 0, statsWaits = 0, statsRequests = 0, statsError = 0, statsTimeouts = 0;
private int statsTotalStatments = 0, statsTotalPrepareds = 0, statsTotalCallable = 0, statsIdleClose = 0, statsInterrupted = 0, statsClosed = 0;
private long statsTotalUsage = 0, statsMaxUsage = 0, statsMinUsage = Long.MAX_VALUE;
private AtomicInteger currentConnections;
private LongTermPoolFactory longTermPoolFactory;
private String dsnKey;
private long timeCreated, timeLastUsed;
public LongTermDataSourcePoolManager(LongTermPoolFactory longTermPoolFactory, cfDataSourceDetails dsDetails) {
this.longTermPoolFactory = longTermPoolFactory;
this.dsnKey = dsDetails.getKey();
this.idleQueue = Collections.synchronizedList(new ArrayList<WrappedConnection>(maxConnections));
this.activeQueue = Collections.synchronizedList(new ArrayList<WrappedConnection>(maxConnections));
this.datasourceName = dsDetails.getDataSourceName();
this.jdbcUri = dsDetails.getHoststring();
this.jdbcUsername = dsDetails.getUsername();
this.jdbcPassword = dsDetails.getPassword();
this.loginTimeoutSecs = dsDetails.getLogintimeout();
this.usageTimeoutMs = dsDetails.getConnectiontimeout() * 1000;
this.maxLiveTimeMs = dsDetails.getMaxLiveTime() * 1000;
this.maxConnections = dsDetails.getLimitconnections();
this.maxUsage = dsDetails.getMaxUsage();
this.initString = dsDetails.getInitString();
this.connectionRetries = dsDetails.getConnectionRetries();
currentConnections = new AtomicInteger();
timeCreated = System.currentTimeMillis();
timeLastUsed = System.currentTimeMillis();
SystemClock.setListenerMinute(this);
}
public void unhookCallback() {
longTermPoolFactory = null;
}
public void clockEvent(int type) {
checkActiveConnections();
checkIdleConnections();
if ( idleQueue.isEmpty() && activeQueue.isEmpty() ){
SystemClock.removeListenerMinute(this);
if ( longTermPoolFactory != null )
longTermPoolFactory.removePoolFactory(dsnKey);
}
}
public Connection getConnection() throws SQLException {
statsRequests++;
timeLastUsed = System.currentTimeMillis();
// maxConnections represents both idle and active connections added together
if (maxConnections == 0) {
return createActiveConnection();
}
long startRequestTime = System.currentTimeMillis();
while (true) {
// Check to see if there are any idle Connections left in the pool
WrappedConnection con = null;
synchronized (idleQueue) {
if (!idleQueue.isEmpty()) {
con = idleQueue.remove(0);
}
}
// Check to see if its still open
if ((con != null) && con.isOpen()) {
con.addConnectionEventListener(this);
synchronized (activeQueue) {
activeQueue.add(con); // Add it to the active queue
}
return con;
}
/*
* If we get here see if we can (or need to) create a new connection synchronize
* to ensure that we do not exceed the maximum number of connections
*/
synchronized (currentConnections) {
if (currentConnections.get() < maxConnections) {
return createActiveConnection();
}
}
// Lets check to make sure they haven't been waiting too long
long timeElapsed = System.currentTimeMillis() - startRequestTime;
if (timeElapsed > (loginTimeoutSecs * 1000)) {
statsTimeouts++;
throw new SQLFailedConnectionException("Timed out waiting for idle connection (waited " + timeElapsed + " ms)");
}
// By the time it has reached here, then it has to wait for a connection
try {
statsWaits++;
Thread.sleep(50);
} catch (InterruptedException ignore) {
}
}
}
// ----------------------------------------------------------------
private Connection createActiveConnection() throws SQLException {
WrappedConnection con = createNewConnectionWithRetries();
currentConnections.incrementAndGet();
con.addConnectionEventListener(this);
synchronized (activeQueue) {
activeQueue.add(con);
}
// Check if the connection needs to be initialized with some SQL statements
if ((initString != null) && (initString.length() > 0)) {
try {
Statement stmt = con.createStatement();
stmt.execute(initString);
stmt.close();
} catch (SQLException sqle) {
SQLException e = new SQLFailedConnectionException("A problem occurred with the initialization string configured for the datasource " + datasourceName + " [init string = " + initString + ", error message = " + sqle.getMessage() + "]", sqle.getSQLState(), sqle.getErrorCode());
throw e;
}
}
return con;
}
/**
* This is to close down a connection that has been taking too long
*
* @param con
*/
private void forceCloseActiveConnection(WrappedConnection con) {
// We need to remove it from the active queue
if ( con.getForceClose() )
return;
con.setForceClose();
synchronized (activeQueue) {
activeQueue.remove(con);
}
currentConnections.decrementAndGet();
}
private void closeActiveConnection(WrappedConnection con) {
con.removeConnectionEventListener(this);
synchronized (activeQueue) {
activeQueue.remove(con);
}
closeUnderlyingConnection(con);
}
public void connectionClosed(ConnectionEvent event) {
// This is called when the connection is closed
WrappedConnection con = (WrappedConnection) event.getSource();
con.removeConnectionEventListener(this);
if (!con.getForceClose() ){
synchronized (activeQueue) {
activeQueue.remove(con);
}
}
// Pull back the stats on this connection
statsTotalUsage += con.getUsageTime();
if (con.getUsageTime() > statsMaxUsage)
statsMaxUsage = con.getUsageTime();
if (con.getUsageTime() < statsMinUsage)
statsMinUsage = con.getUsageTime();
statsTotalStatments += con.getTotalStatements();
statsTotalPrepareds += con.getTotalPrepareds();
statsTotalCallable += con.getTotalCallable();
con.clearStats();
/*
* At this point the connection is in "limbo"... it's not in the activeQueue
* or the idleQueue, we need to figure out whether or not we're going to
* reuse it.
*/
try {
con.clearWarnings();
} catch (SQLException ignore) {}
// Check to see if this connection should NOT be reused again
if (con.getForceClose() || maxConnections == 0 || currentConnections.get() > maxConnections || con.getTotalHits() > maxUsage || con.getAliveTime() > maxLiveTimeMs) {
closeUnderlyingConnection(con);
} else{ // put it back into the pool for reuse
try {
con.setAutoCommit(true);
} catch (SQLException ignore) {}
synchronized (idleQueue) {
idleQueue.add(con);
}
}
}
// ----------------------------------------------------------------
public void connectionErrorOccurred(ConnectionEvent event) {
// This is called when an error occurred with the connection
statsError++;
closeActiveConnection((WrappedConnection) event.getSource());
}
// ----------------------------------------------
private WrappedConnection createNewConnectionWithRetries() throws SQLException {
// connectionRetries indicates how many more attempts should be made
// if the initial newConnection call fails. For example, if it is
// set to 1 then up to 2 attempts will be made
if ( connectionRetries > 0 ){
for (int i = 0; i < connectionRetries; i++) {
try {
return createConnection();
} catch (SQLException ignore) {}
}
}
// Do the last attempt outside the try/catch block so that any
// exception it throws will make its way back to the client.
return createConnection();
}
protected WrappedConnection createConnection() throws SQLException {
/* Let us check to see if the remote server is actually listening or not; we'll only test ones we can parse */
JDBCUrlParser jdbc;
try{
jdbc = new JDBCUrlParser(jdbcUri);
}catch(Exception e){
jdbc = null;
}
if ( jdbc != null && !SocketUtil.isRemotePortAlive( jdbc.getHost(), jdbc.getPort(), SOCKET_WAIT_TIME_MS ) ){
throw new SQLFailedConnectionException( "failed to verify remote server/socket @ " + jdbc.getHost() + ":" + jdbc.getPort() );
}
WrappedConnection con = null;
try {
// need special handling for oracle jdbc connection
if (jdbcUri.startsWith("jdbc:oracle:thin:"))
con = newOracleConnection();
else if (jdbcUsername.length() == 0)
con = new WrappedConnection(DriverManager.getConnection(jdbcUri));
else
con = new WrappedConnection(DriverManager.getConnection(jdbcUri, jdbcUsername, jdbcPassword));
} catch (UnsatisfiedLinkError e) { // can happen with Oracle OCI driver (and other type 2 drivers?)
throw new SQLFailedConnectionException(e.getMessage());
}
// Only increment the statsNewCon counter after the connection has been successfully created
statsNewCon++;
return con;
}
/*
* called if an Oracle connection is required.
*/
private WrappedConnection newOracleConnection() throws SQLException {
int connectStrIndex = jdbcUri.indexOf(';');
String jdbcUriWithoutParams;
java.util.Properties info = new java.util.Properties();
if (jdbcUsername.length() != 0) {
info.put(USER_PROP_NAME, jdbcUsername);
info.put(PASSWORD_PROP_NAME, jdbcPassword);
}
// if there's a connectstring present and it contains at least 1 param
if (connectStrIndex != -1 && connectStrIndex != jdbcUri.length()) {
jdbcUriWithoutParams = jdbcUri.substring(0, connectStrIndex);
// break down the connect string into individual params
String connectStr = jdbcUri.substring(connectStrIndex + 1);
String[] params = string.convertToList(connectStr, ';');
for (int i = 0; i < params.length; i++) {
int splitIndex = params[i].indexOf('=');
if (splitIndex != -1 && splitIndex != params.length) {
info.put(params[i].substring(0, splitIndex), params[i].substring(splitIndex + 1));
} else {
info.put(params[i].substring(0, splitIndex), "");
}
}
} else {
jdbcUriWithoutParams = jdbcUri;
}
return new WrappedConnection(DriverManager.getConnection(jdbcUriWithoutParams, info));
}
// ----------------------------------------------------------------
private void checkActiveConnections() {
synchronized (activeQueue) {
// Need to start at the end of the list since we might remove some elements
for (int i = activeQueue.size() - 1; i >= 0; i--) {
WrappedConnection con = activeQueue.get(i);
/*
* If the connection has been in use for too long, then remove it
* NOTE: this can happen with long running SQL statements and
* connections that have been leaked from the connection pool.
*/
long usageTime = con.getUsageTime();
if (usageTime > usageTimeoutMs) {
cfEngine.log("WARNING: removing connection that has been active too long! (dataSource=" + datasourceName + ", usageTime=" + usageTime + " ms). SQL=" + con.getLastQuery() );
forceCloseActiveConnection(con);
}
}
}
}
private void checkIdleConnections() {
synchronized (idleQueue) {
// Need to start at the end of the list since we might remove some elements
for (int i = idleQueue.size() - 1; i >= 0; i--) {
WrappedConnection con = idleQueue.get(i);
// if been idle close it off
if ( con.getUsageTime() > IDLE_TIMEOUT_MS ) {
statsIdleClose++;
idleQueue.remove(i);
cfEngine.log("Closing idle connection (dataSource=" + datasourceName + ")" );
closeUnderlyingConnection(con);
}
}
}
}
// ----------------------------------------------------------------
private void closeAllIdle() {
synchronized (idleQueue) {
// Need to start at the end of the list since we're removing elements
for (int i = idleQueue.size() - 1; i >= 0; i--) {
WrappedConnection con = idleQueue.remove(i);
closeUnderlyingConnection(con);
}
}
}
private void closeAllActive() {
synchronized (activeQueue) {
// Need to start at the end of the list since we're removing elements
for (int i = activeQueue.size() - 1; i >= 0; i--) {
WrappedConnection con = activeQueue.remove(i);
closeUnderlyingConnection(con);
}
}
}
private void closeUnderlyingConnection(WrappedConnection con) {
statsClosed++;
if ( !con.getForceClose() )
currentConnections.decrementAndGet();
try {
con.getConnection().close();
} catch (SQLException ignore) {}
}
public void closeAll() {
closeAllIdle();
closeAllActive();
SystemClock.removeListenerMinute(this);
}
public boolean isEmpty() {
return (idleQueue.size() + activeQueue.size() == 0) ? true : false;
}
public Map<String,Object> getStatistics() {
Map<String,Object> stats = new FastMap<String,Object>();
stats.put("name", new String(datasourceName));
stats.put("id", new String(jdbcUri + "@" + jdbcUsername));
stats.put("requests", new Integer(statsRequests));
stats.put("connectionsinuse", new Integer(activeQueue.size()));
stats.put("connectionsfree", new Integer(idleQueue.size()));
stats.put("connectionsmax", new Integer(maxConnections));
StringBuilder buffer = new StringBuilder(64);
buffer.append("newconnections=");
buffer.append(statsNewCon);
buffer.append("; waitconnections=");
buffer.append(statsWaits);
buffer.append("; error=");
buffer.append(statsError);
buffer.append("; statements=");
buffer.append(statsTotalStatments);
buffer.append("; preparedstatements=");
buffer.append(statsTotalPrepareds);
buffer.append("; callablestatements=");
buffer.append(statsTotalCallable);
buffer.append("; idleclose=");
buffer.append(statsIdleClose);
buffer.append("; closed=");
buffer.append(statsClosed);
buffer.append("; interrupted=");
buffer.append(statsInterrupted);
buffer.append("; timeouts=");
buffer.append(statsTimeouts);
buffer.append("; totalusagetime=");
buffer.append(statsTotalUsage);
buffer.append("; maxusagetime=");
buffer.append(statsMaxUsage);
buffer.append("; minusagetime=");
buffer.append(statsMinUsage);
stats.put("extra", buffer.toString());
return stats;
}
public cfStructData getPoolStats() {
cfStructData s = new cfStructData();
s.put("dsnname", datasourceName);
s.put("id", jdbcUri + "@" + jdbcUsername );
s.put("requests", statsRequests);
s.setData( "newconnections", statsNewCon );
s.setData( "waitconnections", statsWaits );
s.setData( "totalstatements", statsTotalStatments );
s.setData( "totalpreparedstatements", statsTotalPrepareds );
s.setData( "totalcallablestatements", statsTotalCallable );
s.setData( "idleclose", statsIdleClose );
s.setData( "closed", statsClosed );
s.setData( "interrupted", statsInterrupted );
s.setData( "timeouts", statsTimeouts );
s.setData( "maxusagetime", statsMaxUsage );
s.setData( "minusagetime", statsMinUsage );
s.setData( "created", new cfDateData(timeCreated) );
s.setData( "lastused", new cfDateData(timeLastUsed) );
return s;
}
}