org.mariadb.jdbc.internal.failover.FailoverProxy Maven / Gradle / Ivy
Show all versions of mariadb-java-client Show documentation
/*
*
* MariaDB Client for Java
*
* Copyright (c) 2012-2014 Monty Program Ab.
* Copyright (c) 2015-2020 MariaDB Corporation 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 java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.concurrent.locks.ReentrantLock;
import org.mariadb.jdbc.HostAddress;
import org.mariadb.jdbc.MariaDbConnection;
import org.mariadb.jdbc.MariaDbStatement;
import org.mariadb.jdbc.internal.io.LruTraceCache;
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.ExceptionFactory;
public class FailoverProxy implements InvocationHandler {
private static final String METHOD_IS_EXPLICIT_CLOSED = "isExplicitClosed";
private static final String METHOD_GET_OPTIONS = "getOptions";
private static final String METHOD_GET_URLPARSER = "getUrlParser";
private static final String METHOD_GET_PROXY = "getProxy";
private static final String METHOD_EXECUTE_QUERY = "executeQuery";
private static final String METHOD_SET_READ_ONLY = "setReadonly";
private static final String METHOD_GET_READ_ONLY = "getReadonly";
private static final String METHOD_IS_MASTER_CONNECTION = "isMasterConnection";
private static final String METHOD_VERSION_GREATER_OR_EQUAL = "versionGreaterOrEqual";
private static final String METHOD_SESSION_STATE_AWARE = "sessionStateAware";
private static final String METHOD_CLOSED_EXPLICIT = "closeExplicit";
private static final String METHOD_ABORT = "abort";
private static final String METHOD_IS_CLOSED = "isClosed";
private static final String METHOD_EXECUTE_PREPARED_QUERY = "executePreparedQuery";
private static final String METHOD_COM_MULTI_PREPARE_EXECUTES = "prepareAndExecutesComMulti";
private static final String METHOD_PROLOG_PROXY = "prologProxy";
private static final String METHOD_RESET = "reset";
private static final String METHOD_IS_VALID = "isValid";
private static final String METHOD_GET_LOCK = "getLock";
private static final String METHOD_GET_NO_BACKSLASH = "noBackslashEscapes";
private static final String METHOD_GET_SERVER_THREAD_ID = "getServerThreadId";
private static final String METHOD_PROLOG = "prolog";
private static final String METHOD_GET_CATALOG = "getCatalog";
private static final String METHOD_GET_TIMEOUT = "getTimeout";
private static final String METHOD_GET_MAJOR_VERSION = "getMajorServerVersion";
private static final String METHOD_IN_TRANSACTION = "inTransaction";
private static final String METHOD_IS_MARIADB = "isServerMariaDb";
private static final Logger logger = LoggerFactory.getLogger(FailoverProxy.class);
public final ReentrantLock lock;
public final LruTraceCache traceCache;
private final Listener listener;
/**
* Proxy constructor.
*
* @param listener failover implementation.
* @param lock synchronisation lock
* @param traceCache trace cache
* @throws SQLException if connection error occur
*/
public FailoverProxy(Listener listener, ReentrantLock lock, LruTraceCache traceCache)
throws SQLException {
this.lock = lock;
this.listener = listener;
this.listener.setProxy(this);
this.traceCache = traceCache;
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_GET_LOCK:
return this.lock;
case METHOD_GET_NO_BACKSLASH:
return listener.noBackslashEscapes();
case METHOD_IS_MARIADB:
return listener.isServerMariaDb();
case METHOD_GET_CATALOG:
return listener.getCatalog();
case METHOD_GET_TIMEOUT:
return listener.getTimeout();
case METHOD_VERSION_GREATER_OR_EQUAL:
return listener.versionGreaterOrEqual((int) args[0], (int) args[1], (int) args[2]);
case METHOD_SESSION_STATE_AWARE:
return listener.sessionStateAware();
case METHOD_IS_EXPLICIT_CLOSED:
return listener.isExplicitClosed();
case METHOD_GET_OPTIONS:
return listener.getUrlParser().getOptions();
case METHOD_GET_MAJOR_VERSION:
return listener.getMajorServerVersion();
case METHOD_GET_SERVER_THREAD_ID:
return listener.getServerThreadId();
case METHOD_GET_URLPARSER:
return listener.getUrlParser();
case METHOD_GET_PROXY:
return this;
case METHOD_IS_CLOSED:
return listener.isClosed();
case METHOD_IS_VALID:
return listener.isValid((int) args[0]);
case METHOD_PROLOG:
listener.prolog((long) args[0], (MariaDbConnection) args[2], (MariaDbStatement) args[3]);
return null;
case METHOD_EXECUTE_QUERY:
boolean isClosed = this.listener.isClosed();
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(), isClosed);
}
}
break;
case METHOD_SET_READ_ONLY:
this.listener.switchReadOnlyConnection((Boolean) args[0]);
return null;
case METHOD_GET_READ_ONLY:
return this.listener.isReadOnly();
case METHOD_IN_TRANSACTION:
return this.listener.inTransaction();
case METHOD_IS_MASTER_CONNECTION:
return this.listener.isMasterConnection();
case METHOD_ABORT:
this.listener.preAbort();
return null;
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 replica, but since a failover was running on
// master connection. Replica connection is up
// again, so has to be re-prepared on replica
try {
logger.trace(
"re-prepare query \"{}\" on replica (was temporary on master since failover)",
serverPrepareResult.getSql());
this.listener.rePrepareOnReplica(serverPrepareResult, false);
} catch (SQLException q) {
// error during re-prepare, will do executed on master.
}
}
boolean wasClosed = this.listener.isClosed();
try {
return listener.invoke(method, args, serverPrepareResult.getUnProxiedProtocol());
} catch (InvocationTargetException e) {
if (e.getTargetException() != null) {
if (e.getTargetException() instanceof SQLException
&& hasToHandleFailover((SQLException) e.getTargetException())) {
return handleFailOver(
(SQLException) e.getTargetException(),
method,
args,
serverPrepareResult.getUnProxiedProtocol(),
wasClosed);
}
throw e.getTargetException();
}
throw e;
}
}
break;
case METHOD_PROLOG_PROXY:
boolean wasClosed = this.listener.isClosed();
try {
if (args[0] != null) {
return listener.invoke(
method, args, ((ServerPrepareResult) args[0]).getUnProxiedProtocol());
}
return null;
} catch (InvocationTargetException e) {
if (e.getTargetException() != null) {
if (e.getTargetException() instanceof SQLException
&& hasToHandleFailover((SQLException) e.getTargetException())) {
return handleFailOver(
(SQLException) e.getTargetException(),
method,
args,
((ServerPrepareResult) args[0]).getUnProxiedProtocol(),
wasClosed);
}
throw e.getTargetException();
}
throw e;
}
case METHOD_RESET:
// listener will report reset on any active connections (Master/replica)
listener.reset();
return null;
default:
}
return executeInvocation(method, args, false);
}
private Object executeInvocation(Method method, Object[] args, boolean isSecondExecution)
throws Throwable {
boolean isClosed = listener.isClosed();
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);
// check that failover is due to kill command
boolean killCmd =
queryException != null
&& queryException.getSQLState() != null
&& queryException.getSQLState().equals("70100")
&& 1927 == queryException.getErrorCode();
if (killCmd) {
handleFailOver(queryException, method, args, protocol, isClosed);
return null;
}
if (hasToHandleFailover(queryException)) {
return handleFailOver(queryException, method, args, protocol, isClosed);
}
// error is "The MariaDB server is running with the %s option so it cannot execute this
// statement"
// checking that server was master has not been demote to replica 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, false, isClosed).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(), isClosed);
}
}
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, boolean isClosed)
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, isClosed);
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 if error code is "1927"
*
* @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")
|| (exception.getSQLState().equals("70100") && 1927 == exception.getErrorCode()));
}
/**
* Launch reconnect implementation.
*
* @throws SQLException exception
*/
public void reconnect() throws SQLException {
try {
listener.reconnect();
} catch (SQLException e) {
throw ExceptionFactory.INSTANCE.create(e);
}
}
public Listener getListener() {
return listener;
}
}