/*
MariaDB Client for Java
Copyright (c) 2012-2014 Monty Program 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 info@montyprogram.com.
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.impl;
import org.mariadb.jdbc.HostAddress;
import org.mariadb.jdbc.UrlParser;
import org.mariadb.jdbc.internal.failover.AbstractMastersListener;
import org.mariadb.jdbc.internal.failover.HandleErrorResult;
import org.mariadb.jdbc.internal.failover.thread.FailoverLoop;
import org.mariadb.jdbc.internal.failover.tools.SearchFilter;
import org.mariadb.jdbc.internal.logging.Logger;
import org.mariadb.jdbc.internal.logging.LoggerFactory;
import org.mariadb.jdbc.internal.protocol.MasterProtocol;
import org.mariadb.jdbc.internal.protocol.Protocol;
import org.mariadb.jdbc.internal.util.constant.HaMode;
import org.mariadb.jdbc.internal.util.dao.ReconnectDuringTransactionException;
import org.mariadb.jdbc.internal.util.dao.ServerPrepareResult;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class MastersFailoverListener extends AbstractMastersListener {
private static Logger logger = LoggerFactory.getLogger(MastersFailoverListener.class);
private final HaMode mode;
/**
* Initialisation.
*
* @param urlParser url options.
*/
public MastersFailoverListener(final UrlParser urlParser) {
super(urlParser);
this.mode = urlParser.getHaMode();
setMasterHostFail();
}
/**
* Connect to database.
*
* @throws SQLException if connection is on error.
*/
@Override
public void initializeConnection() throws SQLException {
super.initializeConnection();
this.currentProtocol = null;
//launching initial loop
reconnectFailedConnection(new SearchFilter(true, false));
resetMasterFailoverData();
}
/**
* Before executing query, reconnect if connection is closed, and autoReconnect option is set.
*
* @throws SQLException if connection has been explicitly closed.
*/
public void preExecute() throws SQLException {
lastQueryNanos = System.nanoTime();
//if connection is closed or failed on slave
if (this.currentProtocol != null && this.currentProtocol.isClosed()) {
preAutoReconnect();
}
}
@Override
public void preClose() throws SQLException {
if (explicitClosed.compareAndSet(false, true)) {
proxy.lock.lock();
try {
removeListenerFromSchedulers();
closeConnection(currentProtocol);
} finally {
proxy.lock.unlock();
}
}
}
@Override
public HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable {
boolean alreadyClosed = !currentProtocol.isConnected();
boolean inTransaction = currentProtocol != null && currentProtocol.inTransaction();
proxy.lock.lock();
try {
if (currentProtocol != null && currentProtocol.isConnected() && currentProtocol.ping()) {
//connection re-established
//if in transaction cannot be sure that the last query has been received by server of not,
// so rollback.and throw exception
if (currentProtocol.inTransaction()) {
currentProtocol.rollback();
}
return new HandleErrorResult(true);
}
} catch (SQLException e) {
currentProtocol.close();
if (setMasterHostFail()) {
addToBlacklist(currentProtocol.getHostAddress());
}
} finally {
proxy.lock.unlock();
}
try {
reconnectFailedConnection(new SearchFilter(true, false));
handleFailLoop();
if (alreadyClosed || (!alreadyClosed && !inTransaction && isQueryRelaunchable(method, args))) {
logger.info("Connection to master lost, new master " + currentProtocol.getHostAddress() + " found"
+ ", query type permit to be re-execute on new server without throwing exception");
return relaunchOperation(method, args);
}
return new HandleErrorResult(true);
} catch (Exception e) {
//we will throw a Connection exception that will close connection
FailoverLoop.removeListener(this);
return new HandleErrorResult();
}
}
/**
* Loop to connect failed hosts.
*
* @param searchFilter search parameters.
* @throws SQLException if there is any error during reconnection
*/
@Override
public void reconnectFailedConnection(SearchFilter searchFilter) throws SQLException {
proxy.lock.lock();
try {
if (!searchFilter.isInitialConnection()
&& (isExplicitClosed() || !isMasterHostFail())) {
return;
}
currentConnectionAttempts.incrementAndGet();
resetOldsBlackListHosts();
List<HostAddress> loopAddress = new LinkedList<>(urlParser.getHostAddresses());
if (HaMode.FAILOVER.equals(mode)) {
//put the list in the following order
// - random order not connected host
// - random order blacklist host
// - random order connected host
loopAddress.removeAll(getBlacklistKeys());
Collections.shuffle(loopAddress);
List<HostAddress> blacklistShuffle = new LinkedList<>(getBlacklistKeys());
Collections.shuffle(blacklistShuffle);
loopAddress.addAll(blacklistShuffle);
} else {
//order in sequence
loopAddress.removeAll(getBlacklistKeys());
loopAddress.addAll(getBlacklistKeys());
}
//put connected at end
if (currentProtocol != null && !isMasterHostFail()) {
loopAddress.remove(currentProtocol.getHostAddress());
//loopAddress.add(currentProtocol.getHostAddress());
}
MasterProtocol.loop(this, loopAddress, searchFilter);
//close loop if all connection are retrieved
if (!isMasterHostFail()) {
FailoverLoop.removeListener(this);
}
//if no error, reset failover variables
resetMasterFailoverData();
} finally {
proxy.lock.unlock();
}
}
/**
* Force session to read-only according to options.
*
* @param mustBeReadOnly is read-only flag
* @throws SQLException if a connection error occur
*/
public void switchReadOnlyConnection(Boolean mustBeReadOnly) throws SQLException {
if (urlParser.getOptions().assureReadOnly && currentReadOnlyAsked != mustBeReadOnly) {
proxy.lock.lock();
try {
// verify not updated now that hold lock, double check safe due to volatile
if (currentReadOnlyAsked != mustBeReadOnly) {
currentReadOnlyAsked = mustBeReadOnly;
setSessionReadOnly(mustBeReadOnly, currentProtocol);
}
} finally {
proxy.lock.unlock();
}
}
}
/**
* method called when a new Master connection is found after a fallback.
*
* @param protocol the new active connection
*/
@Override
public void foundActiveMaster(Protocol protocol) throws SQLException {
if (isExplicitClosed()) {
proxy.lock.lock();
try {
protocol.close();
} finally {
proxy.lock.unlock();
}
return;
}
syncConnection(this.currentProtocol, protocol);
proxy.lock.lock();
try {
if (currentProtocol != null && !currentProtocol.isClosed()) {
currentProtocol.close();
}
currentProtocol = protocol;
} finally {
proxy.lock.unlock();
}
resetMasterFailoverData();
FailoverLoop.removeListener(this);
}
/**
* Try to reconnect connection.
*
* @throws SQLException if reconnect a new connection but there was an active transaction.
*/
public void reconnect() throws SQLException {
boolean inTransaction = currentProtocol != null && currentProtocol.inTransaction();
reconnectFailedConnection(new SearchFilter(true, false));
handleFailLoop();
if (inTransaction) {
throw new ReconnectDuringTransactionException("Connection reconnect automatically during an active transaction", 1401, "25S03");
}
}
/**
* Add listener to FailoverLoop if master connection is not active, so a reconnection will be done.
* (the reconnection will be done by failover or if append before by the next query/method that will use
* the failed connection)
* Remove listener from FailoverLoop is master connection is active.
*/
public void handleFailLoop() {
if (isMasterHostFail()) {
if (!isExplicitClosed()) {
FailoverLoop.addListener(this);
}
} else {
FailoverLoop.removeListener(this);
}
}
public boolean isMasterConnected() {
return currentProtocol != null && currentProtocol.isConnected();
}
/**
* Check master status.
*
* @param searchFilter search filter
* @return has some status changed
*/
public boolean checkMasterStatus(SearchFilter searchFilter) {
if (currentProtocol != null) {
pingMasterProtocol(currentProtocol);
}
return false;
}
public void rePrepareOnSlave(ServerPrepareResult oldServerPrepareResult, boolean mustExecuteOnSlave) {
//no slave
}
}