org.mariadb.jdbc.internal.failover.FailoverProxy Maven / Gradle / Ivy
Show all versions of mariadb-java-client-jre7 Show documentation
/*
*
* MariaDB Client for Java
*
* Copyright (c) 2012-2014 Monty Program Ab.
* Copyright (c) 2015-2017 MariaDB Ab.
*
* This library is free software; you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; either version 2.1 of the License, or (at your option)
* any later version.
*
* This library 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License along
* with this library; if not, write to Monty Program Ab [email protected].
*
* This particular MariaDB Client for Java file is work
* derived from a Drizzle-JDBC. Drizzle-JDBC file which is covered by subject to
* the following copyright and notice provisions:
*
* Copyright (c) 2009-2011, Marcus Eriksson
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list
* of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this
* list of conditions and the following disclaimer in the documentation and/or
* other materials provided with the distribution.
*
* Neither the name of the driver nor the names of its contributors may not be
* used to endorse or promote products derived from this software without specific
* prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*
*/
package org.mariadb.jdbc.internal.failover;
import org.mariadb.jdbc.HostAddress;
import org.mariadb.jdbc.internal.logging.Logger;
import org.mariadb.jdbc.internal.logging.LoggerFactory;
import org.mariadb.jdbc.internal.protocol.Protocol;
import org.mariadb.jdbc.internal.util.dao.ServerPrepareResult;
import org.mariadb.jdbc.internal.util.exceptions.ExceptionMapper;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.concurrent.locks.ReentrantLock;
public class FailoverProxy implements InvocationHandler {
public static final String METHOD_IS_EXPLICIT_CLOSED = "isExplicitClosed";
public static final String METHOD_GET_OPTIONS = "getOptions";
public static final String METHOD_GET_PROXY = "getProxy";
public static final String METHOD_EXECUTE_QUERY = "executeQuery";
public static final String METHOD_SET_READ_ONLY = "setReadonly";
public static final String METHOD_IS_READ_ONLY = "isReadOnly";
public static final String METHOD_CLOSED_EXPLICIT = "closeExplicit";
public static final String METHOD_IS_CLOSED = "isClosed";
public static final String METHOD_EXECUTE_PREPARED_QUERY = "executePreparedQuery";
public static final String METHOD_COM_MULTI_PREPARE_EXECUTES = "prepareAndExecutesComMulti";
public static final String METHOD_PROLOG_PROXY = "prologProxy";
private static Logger logger = LoggerFactory.getLogger(FailoverProxy.class);
public final ReentrantLock lock;
private Listener listener;
/**
* Proxy constructor.
*
* @param listener failover implementation.
* @param lock synchronisation lock
* @throws SQLException if connection error occur
*/
public FailoverProxy(Listener listener, ReentrantLock lock) throws SQLException {
this.lock = lock;
this.listener = listener;
this.listener.setProxy(this);
this.listener.initializeConnection();
}
/**
* Add Host information ("on HostAddress...") to exception.
*
* example :
*
* java.sql.SQLException: (conn:603) Cannot execute statement in a READ ONLY transaction.
* Query is: INSERT INTO TableX VALUES (21)
* on HostAddress{host='mydb.example.com', port=3306},master=true
*
* @param exception current exception
* @param protocol protocol to have hostname
*/
private static SQLException addHostInformationToException(SQLException exception, Protocol protocol) {
if (protocol != null) {
return new SQLException(exception.getMessage()
+ "\non " + protocol.getHostAddress().toString() + ",master="
+ protocol.isMasterConnection(), exception.getSQLState(), exception.getErrorCode(), exception.getCause());
}
return exception;
}
/**
* Proxy that catch Protocol call, to permit to catch errors and handle failover when multiple hosts.
*
* @param proxy the current protocol
* @param method the called method on the protocol
* @param args methods parameters
* @return protocol method result
* @throws Throwable the method throwed error if not catch by failover
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
switch (methodName) {
case METHOD_IS_EXPLICIT_CLOSED:
return listener.isExplicitClosed();
case METHOD_GET_OPTIONS:
return listener.getUrlParser().getOptions();
case METHOD_GET_PROXY:
return this;
case METHOD_IS_CLOSED:
return listener.isClosed();
case METHOD_EXECUTE_QUERY:
try {
this.listener.preExecute();
} catch (SQLException e) {
//handle failover only if connection error
//normal error can be thrown upon reconnection if there was a transaction in progress.
if (hasToHandleFailover(e)) {
return handleFailOver(e, method, args, listener.getCurrentProtocol());
}
}
break;
case METHOD_SET_READ_ONLY:
this.listener.switchReadOnlyConnection((Boolean) args[0]);
return null;
case METHOD_IS_READ_ONLY:
return this.listener.isReadOnly();
case METHOD_CLOSED_EXPLICIT:
this.listener.preClose();
return null;
case METHOD_COM_MULTI_PREPARE_EXECUTES:
case METHOD_EXECUTE_PREPARED_QUERY:
boolean mustBeOnMaster = (Boolean) args[0];
ServerPrepareResult serverPrepareResult = (ServerPrepareResult) args[1];
if (serverPrepareResult != null) {
if (!mustBeOnMaster && serverPrepareResult.getUnProxiedProtocol().isMasterConnection() && !this.listener.hasHostFail()) {
//PrepareStatement was to be executed on slave, but since a failover was running on master connection. Slave connection is up
// again, so has to be re-prepared on slave
try {
logger.trace("re-prepare query \"" + serverPrepareResult.getSql() + "\" on slave (was "
+ "temporary on master since failover)");
this.listener.rePrepareOnSlave(serverPrepareResult, mustBeOnMaster);
} catch (SQLException q) {
//error during re-prepare, will do executed on master.
}
}
try {
return listener.invoke(method, args, serverPrepareResult.getUnProxiedProtocol());
} catch (InvocationTargetException e) {
if (e.getTargetException() != null) {
if (e.getTargetException() instanceof SQLException) {
if (hasToHandleFailover((SQLException) e.getTargetException())) {
return handleFailOver((SQLException) e.getTargetException(), method, args,
serverPrepareResult.getUnProxiedProtocol());
}
}
throw e.getTargetException();
}
throw e;
}
}
break;
case METHOD_PROLOG_PROXY:
try {
if (args[0] != null) {
return listener.invoke(method, args, ((ServerPrepareResult) args[0]).getUnProxiedProtocol());
}
} catch (InvocationTargetException e) {
if (e.getTargetException() != null) {
if (e.getTargetException() instanceof SQLException) {
if (hasToHandleFailover((SQLException) e.getTargetException())) {
return handleFailOver((SQLException) e.getTargetException(), method, args,
((ServerPrepareResult) args[0]).getUnProxiedProtocol());
}
}
throw e.getTargetException();
}
throw e;
}
default:
}
return executeInvocation(method, args, false);
}
private Object executeInvocation(Method method, Object[] args, boolean isSecondExecution) throws Throwable {
try {
return listener.invoke(method, args);
} catch (InvocationTargetException e) {
if (e.getTargetException() != null) {
if (e.getTargetException() instanceof SQLException) {
SQLException queryException = (SQLException) e.getTargetException();
Protocol protocol = listener.getCurrentProtocol();
queryException = addHostInformationToException(queryException, protocol);
if (hasToHandleFailover(queryException)) {
return handleFailOver(queryException, method, args, protocol);
}
//error is "The MySQL server is running with the %s option so it cannot execute this statement"
//checking that server was master has not been demote to slave without resetting connections
if (queryException.getErrorCode() == 1290
&& !isSecondExecution
&& protocol != null
&& protocol.isMasterConnection()
&& !protocol.checkIfMaster()) {
boolean inTransaction = protocol.inTransaction();
boolean isReconnected;
//connection state has changed, master connection is now read-only
//reconnect to master, to re-execute command if wasn't in a transaction since
//we are sure has not been executed.
//reconnection
lock.lock();
try {
protocol.close();
isReconnected = listener.primaryFail(null, null).isReconnected;
} finally {
lock.unlock();
}
//relaunch command
if (isReconnected && !inTransaction) {
return executeInvocation(method, args, true);
}
//throw exception if not reconnected, or was in a transaction
return handleFailOver(queryException, method, args, listener.getCurrentProtocol());
}
}
throw e.getTargetException();
}
throw e;
}
}
/**
* After a connection exception, launch failover.
*
* @param qe the exception thrown
* @param method the method to call if failover works well
* @param args the arguments of the method
* @return the object return from the method
* @throws Throwable throwable
*/
private Object handleFailOver(SQLException qe, Method method, Object[] args, Protocol protocol) throws Throwable {
HostAddress failHostAddress = null;
boolean failIsMaster = true;
if (protocol != null) {
failHostAddress = protocol.getHostAddress();
failIsMaster = protocol.isMasterConnection();
}
HandleErrorResult handleErrorResult = listener.handleFailover(qe, method, args, protocol);
if (handleErrorResult.mustThrowError) {
listener.throwFailoverMessage(failHostAddress, failIsMaster, qe, handleErrorResult.isReconnected);
}
return handleErrorResult.resultObject;
}
/**
* Check if this Sqlerror is a connection exception. if that's the case, must be handle by failover
*
* error codes :
* 08000 : connection exception
* 08001 : SQL client unable to establish SQL connection
* 08002 : connection name in use
* 08003 : connection does not exist
* 08004 : SQL server rejected SQL connection
* 08006 : connection failure
* 08007 : transaction resolution unknown
* 70100 : connection was killed
*
* @param exception the Exception
* @return true if there has been a connection error that must be handled by failover
*/
public boolean hasToHandleFailover(SQLException exception) {
return exception.getSQLState() != null && exception.getSQLState().startsWith("08");
}
/**
* Launch reconnect implementation.
*
* @throws SQLException exception
*/
public void reconnect() throws SQLException {
try {
listener.reconnect();
} catch (SQLException e) {
ExceptionMapper.throwException(e, null, null);
}
}
public Listener getListener() {
return listener;
}
}