/*
Copyright (c) 2007, 2013, Oracle and/or its affiliates. All rights reserved.
The MySQL Connector/J is licensed under the terms of the GPLv2
<http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most MySQL Connectors.
There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
this software, see the FLOSS License Exception
<http://www.mysql.com/about/legal/licensing/foss-exception.html>.
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; version 2
of the License.
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.
You should have received a copy of the GNU General Public License along with this
program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
Floor, Boston, MA 02110-1301 USA
*/
package com.mysql.jdbc;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* An implementation of java.sql.Connection that load balances requests across a
* series of MySQL JDBC connections, where the balancing takes place at
* transaction commit.
*
* Therefore, for this to work (at all), you must use transactions, even if only
* reading data.
*
* This implementation will invalidate connections that it detects have had
* communication errors when processing a request. Problematic hosts will be
* added to a global blacklist for loadBalanceBlacklistTimeout ms, after which
* they will be removed from the blacklist and made eligible once again to be
* selected for new connections.
*
* This implementation is thread-safe, but it's questionable whether sharing a
* connection instance amongst threads is a good idea, given that transactions
* are scoped to connections in JDBC.
*
* @version $Id: $
*
*/
public class LoadBalancingConnectionProxy implements InvocationHandler,
PingTarget {
private static Method getLocalTimeMethod;
private long totalPhysicalConnections = 0;
private long activePhysicalConnections = 0;
private String hostToRemove = null;
private long lastUsed = 0;
private long transactionCount = 0;
private ConnectionGroup connectionGroup = null;
protected String closedReason = null;
public static final String BLACKLIST_TIMEOUT_PROPERTY_KEY = "loadBalanceBlacklistTimeout";
static {
try {
getLocalTimeMethod = System.class.getMethod("nanoTime",
new Class[0]);
} catch (SecurityException e) {
// ignore
} catch (NoSuchMethodException e) {
// ignore
}
}
// Lifted from C/J 5.1's JDBC-2.0 connection pool classes, let's merge this
// if/when this gets into 5.1
protected class ConnectionErrorFiringInvocationHandler implements
InvocationHandler {
Object invokeOn = null;
public ConnectionErrorFiringInvocationHandler(Object toInvokeOn) {
invokeOn = toInvokeOn;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
try {
result = method.invoke(invokeOn, args);
if (result != null) {
result = proxyIfInterfaceIsJdbc(result, result.getClass());
}
} catch (InvocationTargetException e) {
dealWithInvocationException(e);
}
return result;
}
}
protected MySQLConnection currentConn;
protected List<String> hostList;
protected Map<String, ConnectionImpl> liveConnections;
private Map<ConnectionImpl, String> connectionsToHostsMap;
private long[] responseTimes;
private Map<String, Integer> hostsToListIndexMap;
private boolean inTransaction = false;
private long transactionStartTime = 0;
private Properties localProps;
protected boolean isClosed = false;
private BalanceStrategy balancer;
private int retriesAllDown;
private static Map<String, Long> globalBlacklist = new HashMap<String, Long>();
private int globalBlacklistTimeout = 0;
private long connectionGroupProxyID = 0;
private LoadBalanceExceptionChecker exceptionChecker;
private Map<Class<?>, Boolean> jdbcInterfacesForProxyCache = new HashMap<Class<?>, Boolean>();
private MySQLConnection thisAsConnection = null;
private int autoCommitSwapThreshold = 0;
private static Constructor<?> JDBC_4_LB_CONNECTION_CTOR;
static {
if(Util.isJdbc4()){
try {
JDBC_4_LB_CONNECTION_CTOR = Class.forName(
"com.mysql.jdbc.JDBC4LoadBalancedMySQLConnection").getConstructor(
new Class[] { LoadBalancingConnectionProxy.class});
} catch (SecurityException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
/**
* Creates a proxy for java.sql.Connection that routes requests between the
* given list of host:port and uses the given properties when creating
* connections.
*
* @param hosts
* @param props
* @throws SQLException
*/
LoadBalancingConnectionProxy(List<String> hosts, Properties props)
throws SQLException {
String group = props.getProperty("loadBalanceConnectionGroup",
null);
boolean enableJMX = false;
String enableJMXAsString = props.getProperty("loadBalanceEnableJMX",
"false");
try{
enableJMX = Boolean.parseBoolean(enableJMXAsString);
} catch (Exception e){
throw SQLError.createSQLException(Messages.getString(
"LoadBalancingConnectionProxy.badValueForLoadBalanceEnableJMX",
new Object[] { enableJMXAsString }),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
}
if(group != null){
this.connectionGroup = ConnectionGroupManager.getConnectionGroupInstance(group);
if(enableJMX){
ConnectionGroupManager.registerJmx();
}
this.connectionGroupProxyID = this.connectionGroup.registerConnectionProxy(this, hosts);
hosts = new ArrayList<String>(this.connectionGroup.getInitialHosts());
}
this.hostList = hosts;
int numHosts = this.hostList.size();
this.liveConnections = new HashMap<String, ConnectionImpl>(numHosts);
this.connectionsToHostsMap = new HashMap<ConnectionImpl, String>(numHosts);
this.responseTimes = new long[numHosts];
this.hostsToListIndexMap = new HashMap<String, Integer>(numHosts);
this.localProps = (Properties) props.clone();
this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY);
this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY);
for (int i = 0; i < numHosts; i++) {
this.hostsToListIndexMap.put(this.hostList.get(i), Integer.valueOf(i));
this.localProps.remove(NonRegisteringDriver.HOST_PROPERTY_KEY + "."
+ (i + 1));
this.localProps.remove(NonRegisteringDriver.PORT_PROPERTY_KEY + "."
+ (i + 1));
}
this.localProps.remove(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY);
this.localProps.setProperty("useLocalSessionState", "true");
String strategy = this.localProps.getProperty("loadBalanceStrategy",
"random");
String lbExceptionChecker = this.localProps.getProperty("loadBalanceExceptionChecker",
"com.mysql.jdbc.StandardLoadBalanceExceptionChecker");
String retriesAllDownAsString = this.localProps.getProperty(
"retriesAllDown", "120");
try {
this.retriesAllDown = Integer.parseInt(retriesAllDownAsString);
} catch (NumberFormatException nfe) {
throw SQLError.createSQLException(Messages.getString(
"LoadBalancingConnectionProxy.badValueForRetriesAllDown",
new Object[] { retriesAllDownAsString }),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
}
String blacklistTimeoutAsString = this.localProps.getProperty(
BLACKLIST_TIMEOUT_PROPERTY_KEY, "0");
try {
this.globalBlacklistTimeout = Integer
.parseInt(blacklistTimeoutAsString);
} catch (NumberFormatException nfe) {
throw SQLError
.createSQLException(
Messages
.getString(
"LoadBalancingConnectionProxy.badValueForLoadBalanceBlacklistTimeout",
new Object[] { retriesAllDownAsString }),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
}
if ("random".equals(strategy)) {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
"com.mysql.jdbc.RandomBalanceStrategy",
"InvalidLoadBalanceStrategy", null).get(0);
} else if ("bestResponseTime".equals(strategy)) {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
"com.mysql.jdbc.BestResponseTimeBalanceStrategy",
"InvalidLoadBalanceStrategy", null).get(0);
} else {
this.balancer = (BalanceStrategy) Util.loadExtensions(null, props,
strategy, "InvalidLoadBalanceStrategy", null).get(0);
}
String autoCommitSwapThresholdAsString = props.getProperty("loadBalanceAutoCommitStatementThreshold",
"0");
try {
this.autoCommitSwapThreshold = Integer.parseInt(autoCommitSwapThresholdAsString);
} catch (NumberFormatException nfe) {
throw SQLError.createSQLException(Messages.getString(
"LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementThreshold",
new Object[] { autoCommitSwapThresholdAsString }),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
}
String autoCommitSwapRegex = props.getProperty("loadBalanceAutoCommitStatementRegex","");
if(!("".equals(autoCommitSwapRegex))){
try{
"".matches(autoCommitSwapRegex);
} catch (Exception e){
throw SQLError.createSQLException(Messages.getString(
"LoadBalancingConnectionProxy.badValueForLoadBalanceAutoCommitStatementRegex",
new Object[] { autoCommitSwapRegex }),
SQLError.SQL_STATE_ILLEGAL_ARGUMENT, null);
}
}
if(this.autoCommitSwapThreshold > 0){
String statementInterceptors = this.localProps.getProperty("statementInterceptors");
if(statementInterceptors == null){
this.localProps.setProperty("statementInterceptors", "com.mysql.jdbc.LoadBalancedAutoCommitInterceptor");
} else if(statementInterceptors.length() > 0){
this.localProps.setProperty("statementInterceptors", statementInterceptors + ",com.mysql.jdbc.LoadBalancedAutoCommitInterceptor");
}
props.setProperty("statementInterceptors", this.localProps.getProperty("statementInterceptors"));
}
this.balancer.init(null, props);
this.exceptionChecker = (LoadBalanceExceptionChecker) Util.loadExtensions(null, props,
lbExceptionChecker, "InvalidLoadBalanceExceptionChecker", null).get(0);
this.exceptionChecker.init(null, props);
if(Util.isJdbc4() || JDBC_4_LB_CONNECTION_CTOR != null){
thisAsConnection = (MySQLConnection) Util.handleNewInstance(JDBC_4_LB_CONNECTION_CTOR,
new Object[] {this}, null);
}else{
thisAsConnection = new LoadBalancedMySQLConnection(this);
}
pickNewConnection();
}
/**
* Creates a new physical connection for the given host:port and updates
* required internal mappings and statistics for that connection.
*
* @param hostPortSpec
* @return
* @throws SQLException
*/
public synchronized ConnectionImpl createConnectionForHost(
String hostPortSpec) throws SQLException {
Properties connProps = (Properties) this.localProps.clone();
String[] hostPortPair = NonRegisteringDriver
.parseHostPortPair(hostPortSpec);
String hostName = hostPortPair[NonRegisteringDriver.HOST_NAME_INDEX];
String portNumber = hostPortPair[NonRegisteringDriver.PORT_NUMBER_INDEX];
String dbName = connProps
.getProperty(NonRegisteringDriver.DBNAME_PROPERTY_KEY);
if (hostName == null) {
throw new SQLException(
"Could not find a hostname to start a connection to");
}
if (portNumber == null) {
portNumber = "3306";// use default
}
connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY, hostName);
connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY,
portNumber);
connProps.setProperty(NonRegisteringDriver.HOST_PROPERTY_KEY + ".1",
hostName);
connProps.setProperty(NonRegisteringDriver.PORT_PROPERTY_KEY + ".1",
portNumber);
connProps.setProperty(NonRegisteringDriver.NUM_HOSTS_PROPERTY_KEY, "1");
connProps.setProperty("roundRobinLoadBalance", "false"); // make sure we
// don't
// pickup
// the
// default
// value
ConnectionImpl conn = (ConnectionImpl) ConnectionImpl.getInstance(
hostName, Integer.parseInt(portNumber), connProps, dbName,
"jdbc:mysql://" + hostName + ":" + portNumber + "/");
this.liveConnections.put(hostPortSpec, conn);
this.connectionsToHostsMap.put(conn, hostPortSpec);
this.activePhysicalConnections++;
this.totalPhysicalConnections++;
conn.setProxy(this.thisAsConnection);
conn.setRealProxy(this);
return conn;
}
/**
* @param e
* @throws SQLException
* @throws Throwable
* @throws InvocationTargetException
*/
void dealWithInvocationException(InvocationTargetException e)
throws SQLException, Throwable, InvocationTargetException {
Throwable t = e.getTargetException();
if (t != null) {
if (t instanceof SQLException && shouldExceptionTriggerFailover((SQLException) t )) {
invalidateCurrentConnection();
pickNewConnection();
}
throw t;
}
throw e;
}
/**
* Closes current connection and removes it from required mappings.
*
* @throws SQLException
*/
synchronized void invalidateCurrentConnection() throws SQLException {
invalidateConnection(this.currentConn);
}
/**
* Closes specified connection and removes it from required mappings.
* @param conn
* @throws SQLException
*/
synchronized void invalidateConnection(MySQLConnection conn) throws SQLException {
try {
if (!conn.isClosed()) {
conn.close();
}
} catch (SQLException e) {
// we don't really want to throw this Exception
} finally {
// add host to the global blacklist, if enabled
if (this.isGlobalBlacklistEnabled()) {
this.addToGlobalBlacklist(this.connectionsToHostsMap
.get(conn));
}
// remove from liveConnections
this.liveConnections.remove(this.connectionsToHostsMap
.get(conn));
Object mappedHost = this.connectionsToHostsMap
.remove(conn);
if (mappedHost != null
&& this.hostsToListIndexMap.containsKey(mappedHost)) {
int hostIndex = (this.hostsToListIndexMap
.get(mappedHost)).intValue();
// reset the statistics for the host
synchronized (this.responseTimes) {
this.responseTimes[hostIndex] = 0;
}
}
}
}
private void closeAllConnections() {
synchronized (this) {
// close all underlying connections
Iterator<ConnectionImpl> allConnections = this.liveConnections.values().iterator();
while (allConnections.hasNext()) {
try {
this.activePhysicalConnections--;
allConnections.next().close();
} catch (SQLException e) {
}
}
if (!this.isClosed) {
this.balancer.destroy();
if(this.connectionGroup != null){
this.connectionGroup.closeConnectionProxy(this);
}
}
this.liveConnections.clear();
this.connectionsToHostsMap.clear();
}
}
private void abortAllConnectionsInternal() {
synchronized (this) {
// abort all underlying connections
Iterator<ConnectionImpl> allConnections = this.liveConnections.values().iterator();
while (allConnections.hasNext()) {
try {
this.activePhysicalConnections--;
allConnections.next().abortInternal();
} catch (SQLException e) {
}
}
if (!this.isClosed) {
this.balancer.destroy();
if(this.connectionGroup != null){
this.connectionGroup.closeConnectionProxy(this);
}
}
this.liveConnections.clear();
this.connectionsToHostsMap.clear();
}
}
private void abortAllConnections(Executor executor) {
synchronized (this) {
// close all underlying connections
Iterator<ConnectionImpl> allConnections = this.liveConnections.values().iterator();
while (allConnections.hasNext()) {
try {
this.activePhysicalConnections--;
allConnections.next().abort(executor);
} catch (SQLException e) {
}
}
if (!this.isClosed) {
this.balancer.destroy();
if(this.connectionGroup != null){
this.connectionGroup.closeConnectionProxy(this);
}
}
this.liveConnections.clear();
this.connectionsToHostsMap.clear();
}
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return this.invoke(proxy, method, args, true);
}
/**
* Proxies method invocation on the java.sql.Connection interface, trapping
* "close", "isClosed" and "commit/rollback" (to switch connections for load
* balancing).
*
* @param proxy
* @param method
* @param args
* @param swapAtTransactionBoundary
* @return
* @throws Throwable
*/
public synchronized Object invoke(Object proxy, Method method, Object[] args, boolean swapAtTransactionBoundary)
throws Throwable {
String methodName = method.getName();
if("getLoadBalanceSafeProxy".equals(methodName)){
return this.currentConn;
}
if ("equals".equals(methodName) && args.length == 1) {
if (args[0] instanceof Proxy) {
return Boolean.valueOf((((Proxy) args[0]).equals(this)));
}
return Boolean.valueOf(this.equals(args[0]));
}
if ("hashCode".equals(methodName)) {
return Integer.valueOf(this.hashCode());
}
if ("close".equals(methodName)) {
closeAllConnections();
this.isClosed = true;
this.closedReason = "Connection explicitly closed.";
return null;
}
if ("abortInternal".equals(methodName)) {
abortAllConnectionsInternal();
this.isClosed = true;
this.closedReason = "Connection explicitly closed.";
return null;
}
if ("abort".equals(methodName) && args.length == 1) {
abortAllConnections((Executor)args[0]);
this.isClosed = true;
this.closedReason = "Connection explicitly closed.";
return null;
}
if ("isClosed".equals(methodName)) {
return Boolean.valueOf(this.isClosed);
}
if (this.isClosed) {
String reason = "No operations allowed after connection closed.";
if(this.closedReason != null){
reason += (" " + this.closedReason);
}
throw SQLError.createSQLException(
reason,
SQLError.SQL_STATE_CONNECTION_NOT_OPEN, null /*
* no access to
* a interceptor
* here...
*/);
}
if (!inTransaction) {
this.inTransaction = true;
this.transactionStartTime = getLocalTimeBestResolution();
this.transactionCount++;
}
Object result = null;
try {
this.lastUsed = System.currentTimeMillis();
result = method.invoke(thisAsConnection, args);
if (result != null) {
if (result instanceof com.mysql.jdbc.Statement) {
((com.mysql.jdbc.Statement) result).setPingTarget(this);
}
result = proxyIfInterfaceIsJdbc(result, result.getClass());
}
} catch (InvocationTargetException e) {
dealWithInvocationException(e);
} finally {
if (swapAtTransactionBoundary && ("commit".equals(methodName) || "rollback".equals(methodName))) {
this.inTransaction = false;
// Update stats
String host = this.connectionsToHostsMap.get(this.currentConn);
// avoid NPE if the connection has already been removed from
// connectionsToHostsMap
// in invalidateCurrenctConnection()
if (host != null) {
synchronized (this.responseTimes) {
int hostIndex = (this.hostsToListIndexMap
.get(host)).intValue();
if(hostIndex < this.responseTimes.length){
this.responseTimes[hostIndex] = getLocalTimeBestResolution()
- this.transactionStartTime;
}
}
}
pickNewConnection();
}
}
return result;
}
/**
* Picks the "best" connection to use for the next transaction based on the
* BalanceStrategy in use.
*
* @throws SQLException
*/
protected synchronized void pickNewConnection() throws SQLException {
if (this.isClosed && "Connection explicitly closed.".equals(this.closedReason)) {
return;
}
if (this.currentConn == null) { // startup
this.currentConn = this.balancer.pickConnection(this, Collections
.unmodifiableList(this.hostList), Collections
.unmodifiableMap(this.liveConnections),
this.responseTimes.clone(), this.retriesAllDown);
return;
}
if(this.currentConn.isClosed()){
invalidateCurrentConnection();
}
int pingTimeout = this.currentConn.getLoadBalancePingTimeout();
boolean pingBeforeReturn = this.currentConn.getLoadBalanceValidateConnectionOnSwapServer();
for(int hostsTried = 0, hostsToTry = this.hostList.size(); hostsTried <= hostsToTry; hostsTried++){
ConnectionImpl newConn = null;
try{
newConn = this.balancer.pickConnection(
this, Collections.unmodifiableList(this.hostList), Collections
.unmodifiableMap(this.liveConnections),
this.responseTimes.clone(), this.retriesAllDown);
if (this.currentConn != null) {
if(pingBeforeReturn){
if(pingTimeout == 0){
newConn.ping();
} else {
newConn.pingInternal(true, pingTimeout);
}
}
syncSessionState(this.currentConn, newConn);
}
this.currentConn = newConn;
return;
} catch (SQLException e){
if (shouldExceptionTriggerFailover(e) && newConn != null) {
// connection error, close up shop on current
// connection
invalidateConnection(newConn);
}
}
}
// no hosts available to swap connection to, close up.
this.isClosed = true;
this.closedReason = "Connection closed after inability to pick valid new connection during fail-over.";
}
/**
* Recursively checks for interfaces on the given object to determine if it
* implements a java.sql interface, and if so, proxies the instance so that
* we can catch and fire SQL errors.
*
* @param toProxy
* @param clazz
* @return
*/
Object proxyIfInterfaceIsJdbc(Object toProxy, Class<?> clazz) {
if(isInterfaceJdbc(clazz)){
Class<?>[] interfacesToProxy = getAllInterfacesToProxy(clazz);
return Proxy.newProxyInstance(toProxy.getClass()
.getClassLoader(), interfacesToProxy,
createConnectionProxy(toProxy));
}
return toProxy;
}
private Map<Class<?>, Class<?>[]> allInterfacesToProxy = new HashMap<Class<?>, Class<?>[]>();
private Class<?>[] getAllInterfacesToProxy(Class<?> clazz) {
Class<?>[] interfacesToProxy = this.allInterfacesToProxy.get(clazz);
if (interfacesToProxy != null) {
return interfacesToProxy;
}
List<Class<?>> interfaces = new LinkedList<Class<?>>();
Class<?> superClass = clazz;
while (!(superClass.equals(Object.class))) {
Class<?>[] declared = superClass.getInterfaces();
for (int i = 0; i < declared.length; i++) {
interfaces.add(declared[i]);
}
superClass = superClass.getSuperclass();
}
interfacesToProxy = new Class[interfaces.size()];
interfaces.toArray(interfacesToProxy);
this.allInterfacesToProxy.put(clazz, interfacesToProxy);
return interfacesToProxy;
}
private boolean isInterfaceJdbc(Class<?> clazz){
if(this.jdbcInterfacesForProxyCache.containsKey(clazz)){
return (this.jdbcInterfacesForProxyCache.get(clazz)).booleanValue();
}
Class<?>[] interfaces = clazz.getInterfaces();
for (int i = 0; i < interfaces.length; i++) {
String packageName = interfaces[i].getPackage().getName();
if ("java.sql".equals(packageName)
|| "javax.sql".equals(packageName)
|| "com.mysql.jdbc".equals(packageName)) {
this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true));
return true;
}
if(isInterfaceJdbc(interfaces[i])){
this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(true));
return true;
}
}
this.jdbcInterfacesForProxyCache.put(clazz, Boolean.valueOf(false));
return false;
}
protected ConnectionErrorFiringInvocationHandler createConnectionProxy(
Object toProxy) {
return new ConnectionErrorFiringInvocationHandler(toProxy);
}
/**
* Returns best-resolution representation of local time, using nanoTime() if
* available, otherwise defaulting to currentTimeMillis().
*/
private static long getLocalTimeBestResolution() {
if (getLocalTimeMethod != null) {
try {
return ((Long) getLocalTimeMethod.invoke(null, (Object[])null))
.longValue();
} catch (IllegalArgumentException e) {
// ignore - we fall through to currentTimeMillis()
} catch (IllegalAccessException e) {
// ignore - we fall through to currentTimeMillis()
} catch (InvocationTargetException e) {
// ignore - we fall through to currentTimeMillis()
}
}
return System.currentTimeMillis();
}
public synchronized void doPing() throws SQLException {
SQLException se = null;
boolean foundHost = false;
int pingTimeout = this.currentConn.getLoadBalancePingTimeout();
synchronized (this) {
for (Iterator<String> i = this.hostList.iterator(); i.hasNext();) {
String host = i.next();
ConnectionImpl conn = this.liveConnections.get(host);
if (conn == null) {
continue;
}
try {
if(pingTimeout == 0){
conn.ping();
} else {
conn.pingInternal(true, pingTimeout);
}
foundHost = true;
} catch (SQLException e) {
this.activePhysicalConnections--;
// give up if it is the current connection, otherwise NPE
// faking resultset later.
if (host.equals(this.connectionsToHostsMap
.get(this.currentConn))) {
// clean up underlying connections, since connection
// pool won't do it
closeAllConnections();
this.isClosed = true;
this.closedReason = "Connection closed because ping of current connection failed.";
throw e;
}
// if the Exception is caused by ping connection lifetime
// checks, don't add to blacklist
if (e
.getMessage()
.equals(
Messages
.getString("Connection.exceededConnectionLifetime"))) {
// only set the return Exception if it's null
if (se == null) {
se = e;
}
} else {
// overwrite the return Exception no matter what
se = e;
if (this.isGlobalBlacklistEnabled()) {
this.addToGlobalBlacklist(host);
}
}
// take the connection out of the liveConnections Map
this.liveConnections.remove(this.connectionsToHostsMap
.get(conn));
}
}
}
// if there were no successful pings
if (!foundHost) {
closeAllConnections();
this.isClosed = true;
this.closedReason = "Connection closed due to inability to ping any active connections.";
// throw the stored Exception, if exists
if (se != null) {
throw se;
}
// or create a new SQLException and throw it, must be no
// liveConnections
((ConnectionImpl) this.currentConn)
.throwConnectionClosedException();
}
}
public void addToGlobalBlacklist(String host, long timeout) {
if (this.isGlobalBlacklistEnabled()) {
synchronized (globalBlacklist) {
globalBlacklist.put(host, Long.valueOf(timeout));
}
}
}
public void addToGlobalBlacklist(String host){
addToGlobalBlacklist(host, System.currentTimeMillis()
+ this.globalBlacklistTimeout);
}
public boolean isGlobalBlacklistEnabled() {
return (this.globalBlacklistTimeout > 0);
}
public synchronized Map<String, Long> getGlobalBlacklist() {
if (!this.isGlobalBlacklistEnabled()) {
String localHostToRemove = this.hostToRemove;
if(hostToRemove != null){
HashMap<String, Long> fakedBlacklist = new HashMap<String, Long>();
fakedBlacklist.put(localHostToRemove, Long.valueOf(System.currentTimeMillis() + 5000));
return fakedBlacklist;
}
return new HashMap<String, Long>(1);
}
// Make a local copy of the blacklist
Map<String, Long> blacklistClone = new HashMap<String, Long>(globalBlacklist.size());
// Copy everything from synchronized global blacklist to local copy for
// manipulation
synchronized (globalBlacklist) {
blacklistClone.putAll(globalBlacklist);
}
Set<String> keys = blacklistClone.keySet();
// we're only interested in blacklisted hosts that are in the hostList
keys.retainAll(this.hostList);
// Don't need to synchronize here as we using a local copy
for (Iterator<String> i = keys.iterator(); i.hasNext();) {
String host = i.next();
// OK if null is returned because another thread already purged Map
// entry.
Long timeout = globalBlacklist.get(host);
if (timeout != null
&& timeout.longValue() < System.currentTimeMillis()) {
// Timeout has expired, remove from blacklist
synchronized (globalBlacklist) {
globalBlacklist.remove(host);
}
i.remove();
}
}
if (keys.size() == this.hostList.size()) {
// return an empty blacklist, let the BalanceStrategy
// implementations try to connect to everything
// since it appears that all hosts are unavailable - we don't want
// to wait for
// loadBalanceBlacklistTimeout to expire.
return new HashMap<String, Long>(1);
}
return blacklistClone;
}
public boolean shouldExceptionTriggerFailover(SQLException ex){
return this.exceptionChecker.shouldExceptionTriggerFailover(ex);
}
public void removeHostWhenNotInUse(String host)
throws SQLException {
int timeBetweenChecks = 1000;
long timeBeforeHardFail = 15000;
synchronized (this) {
addToGlobalBlacklist(host, timeBeforeHardFail + 1000);
long cur = System.currentTimeMillis();
while (System.currentTimeMillis() - timeBeforeHardFail < cur) {
this.hostToRemove = host;
if (!host.equals(this.currentConn.getHost())) {
removeHost(host);
return;
}
}
}
try {
Thread.sleep(timeBetweenChecks);
} catch (InterruptedException e) {
// better to swallow this and retry.
}
removeHost(host);
}
public synchronized void removeHost(String host) throws SQLException {
if (this.connectionGroup != null) {
if (this.connectionGroup.getInitialHosts().size() == 1
&& this.connectionGroup.getInitialHosts().contains(host)) {
throw SQLError.createSQLException(
"Cannot remove only configured host.", null);
}
this.hostToRemove = host;
if (host.equals(this.currentConn.getHost())) {
closeAllConnections();
} else {
this.connectionsToHostsMap.remove(this.liveConnections
.remove(host));
Integer idx = this.hostsToListIndexMap.remove(host);
long[] newResponseTimes = new long[this.responseTimes.length - 1];
int newIdx = 0;
for (Iterator<String> i = this.hostList.iterator(); i.hasNext(); newIdx++) {
String copyHost = i.next();
if (idx != null
&& idx.intValue() < this.responseTimes.length) {
newResponseTimes[newIdx] = this.responseTimes[idx
.intValue()];
this.hostsToListIndexMap.put(copyHost,
Integer.valueOf(newIdx));
}
}
this.responseTimes = newResponseTimes;
}
}
}
public synchronized boolean addHost(String host) {
if (this.hostsToListIndexMap.containsKey(host)) {
return false;
}
long[] newResponseTimes = new long[this.responseTimes.length + 1];
for (int i = 0; i < this.responseTimes.length; i++) {
newResponseTimes[i] = this.responseTimes[i];
}
this.responseTimes = newResponseTimes;
this.hostList.add(host);
this.hostsToListIndexMap.put(host,
Integer.valueOf(this.responseTimes.length - 1));
return true;
}
public synchronized long getLastUsed(){
return this.lastUsed;
}
public synchronized boolean inTransaction(){
return this.inTransaction;
}
public synchronized long getTransactionCount(){
return this.transactionCount;
}
public synchronized long getActivePhysicalConnectionCount(){
return this.activePhysicalConnections;
}
public synchronized long getTotalPhysicalConnectionCount(){
return this.totalPhysicalConnections;
}
public synchronized long getConnectionGroupProxyID(){
return this.connectionGroupProxyID;
}
public synchronized String getCurrentActiveHost() {
MySQLConnection c = this.currentConn;
if(c != null){
Object o = this.connectionsToHostsMap.get(c);
if(o != null){
return o.toString();
}
}
return null;
}
public synchronized long getCurrentTransactionDuration(){
if (this.inTransaction && this.transactionStartTime > 0) {
return getLocalTimeBestResolution() - this.transactionStartTime;
}
return 0;
}
protected void syncSessionState(Connection initial, Connection target)
throws SQLException {
if (initial == null || target == null) {
return;
}
target.setAutoCommit(initial.getAutoCommit());
target.setCatalog(initial.getCatalog());
target.setTransactionIsolation(initial.getTransactionIsolation());
target.setReadOnly(initial.isReadOnly());
}
}