From 9376624872c3cf69c620535b4c6ceabfdea71c26 Mon Sep 17 00:00:00 2001 From: kolzeq Date: Sun, 17 May 2015 17:59:31 +0200 Subject: [PATCH] Failover handling --- src/main/java/org/mariadb/jdbc/Driver.java | 21 +- .../java/org/mariadb/jdbc/HostAddress.java | 42 ++- src/main/java/org/mariadb/jdbc/JDBCUrl.java | 30 +- .../org/mariadb/jdbc/MySQLConnection.java | 18 +- .../org/mariadb/jdbc/MySQLDataSource.java | 15 +- .../mariadb/jdbc/MySQLDatabaseMetaData.java | 5 +- .../java/org/mariadb/jdbc/MySQLResultSet.java | 21 +- .../MySQLServerSidePreparedStatement.java | 5 +- .../java/org/mariadb/jdbc/MySQLStatement.java | 37 +- .../org/mariadb/jdbc/MySQLXAResource.java | 1 - .../jdbc/internal/SQLExceptionMapper.java | 2 +- .../internal/mysql/AuroraHostListener.java | 117 ++++++ .../mysql/AuroraMultiNodesProtocol.java | 149 ++++++++ .../jdbc/internal/mysql/ConnectorUtils.java | 41 ++ .../jdbc/internal/mysql/FailoverListener.java | 65 ++++ .../jdbc/internal/mysql/FailoverProxy.java | 246 ++++++++++++ .../internal/mysql/HandleErrorResult.java | 55 +++ .../internal/mysql/MultiHostListener.java | 354 ++++++++++++++++++ .../internal/mysql/MultiNodesProtocol.java | 157 ++++++++ .../jdbc/internal/mysql/MySQLProtocol.java | 270 ++++++------- .../mariadb/jdbc/internal/mysql/Protocol.java | 103 +++++ .../internal/mysql/SingleHostListener.java | 144 +++++++ .../org/mariadb/jdbc/AuroraFailoverTest.java | 93 +++++ src/test/java/org/mariadb/jdbc/Failover.java | 90 ----- .../java/org/mariadb/jdbc/FailoverTest.java | 100 +++++ 25 files changed, 1852 insertions(+), 329 deletions(-) create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/AuroraHostListener.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/AuroraMultiNodesProtocol.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/ConnectorUtils.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/FailoverListener.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/FailoverProxy.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/HandleErrorResult.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/MultiHostListener.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/MultiNodesProtocol.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/Protocol.java create mode 100644 src/main/java/org/mariadb/jdbc/internal/mysql/SingleHostListener.java create mode 100644 src/test/java/org/mariadb/jdbc/AuroraFailoverTest.java delete mode 100644 src/test/java/org/mariadb/jdbc/Failover.java create mode 100644 src/test/java/org/mariadb/jdbc/FailoverTest.java diff --git a/src/main/java/org/mariadb/jdbc/Driver.java b/src/main/java/org/mariadb/jdbc/Driver.java index 230c090a0..d728ad645 100644 --- a/src/main/java/org/mariadb/jdbc/Driver.java +++ b/src/main/java/org/mariadb/jdbc/Driver.java @@ -52,8 +52,9 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.SQLExceptionMapper; import org.mariadb.jdbc.internal.common.QueryException; import org.mariadb.jdbc.internal.common.Utils; -import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.*; +import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.DriverManager; import java.sql.DriverPropertyInfo; @@ -87,7 +88,7 @@ public final class Driver implements java.sql.Driver { * @return a connection * @throws SQLException if it is not possible to connect */ - public Connection connect(final String url, final Properties info) throws SQLException { + public Connection connect(final String url, final Properties props) throws SQLException { // TODO: handle the properties! // TODO: define what props we support! @@ -96,7 +97,7 @@ public Connection connect(final String url, final Properties info) throws SQLExc if(idx > 0) { baseUrl = url.substring(0,idx); String urlParams = url.substring(idx+1); - setURLParameters(urlParams, info); + setURLParameters(urlParams, props); } log.finest("Connecting to: " + url); @@ -105,12 +106,18 @@ public Connection connect(final String url, final Properties info) throws SQLExc if(jdbcUrl == null) { return null; } - String userName = info.getProperty("user",jdbcUrl.getUsername()); - String password = info.getProperty("password",jdbcUrl.getPassword()); + String username = props.getProperty("user",jdbcUrl.getUsername()); + String password = props.getProperty("password",jdbcUrl.getPassword()); - MySQLProtocol protocol = new MySQLProtocol(jdbcUrl, userName, password, info); + if (jdbcUrl.getHostAddresses() == null) { + log.info("MariaDB connector : missing Host address"); + return null; + } else { + Protocol proxyfiedProtocol = ConnectorUtils.retrieveProxy(jdbcUrl, username, password, props); + proxyfiedProtocol.initializeConnection(); + return MySQLConnection.newConnection(proxyfiedProtocol); + } - return MySQLConnection.newConnection(protocol); } catch (QueryException e) { SQLExceptionMapper.throwException(e, null, null); return null; diff --git a/src/main/java/org/mariadb/jdbc/HostAddress.java b/src/main/java/org/mariadb/jdbc/HostAddress.java index 8c21e9e15..e22a813ff 100644 --- a/src/main/java/org/mariadb/jdbc/HostAddress.java +++ b/src/main/java/org/mariadb/jdbc/HostAddress.java @@ -1,5 +1,7 @@ package org.mariadb.jdbc; +import java.util.List; + public class HostAddress { public String host; public int port; @@ -12,7 +14,8 @@ public class HostAddress { * @return parsed endpoints */ public static HostAddress[] parse(String spec) { - String[] tokens = spec.split(","); + if (spec == null) return null; + String[] tokens = spec.trim().split(","); HostAddress[] arr = new HostAddress[tokens.length]; for (int i=0; i < tokens.length; i++) { @@ -53,6 +56,19 @@ else if (s.contains(":")) { } return result; } + + public static String toString(List addrs) { + String s=""; + for(int i=0; i < addrs.size(); i++) { + boolean isIPv6 = addrs.get(i).host != null && addrs.get(i).host.contains(":"); + String host = (isIPv6)?("[" + addrs.get(i).host + "]"):addrs.get(i).host; + s += host + ":" + addrs.get(i).port; + if (i < addrs.size() -1) + s += ","; + } + return s; + } + public static String toString(HostAddress[] addrs) { String s=""; for(int i=0; i < addrs.length; i++) { @@ -64,5 +80,29 @@ public static String toString(HostAddress[] addrs) { } return s; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof HostAddress)) return false; + + HostAddress that = (HostAddress) o; + + if (port != that.port) return false; + return !(host != null ? !host.equals(that.host) : that.host != null); + + } + + @Override + public int hashCode() { + int result = host != null ? host.hashCode() : 0; + result = 31 * result + port; + return result; + } + + @Override + public String toString() { + return "HostAddress{" + host + ":" + port + "}"; + } } diff --git a/src/main/java/org/mariadb/jdbc/JDBCUrl.java b/src/main/java/org/mariadb/jdbc/JDBCUrl.java index 8d2909db9..2a9a2283e 100644 --- a/src/main/java/org/mariadb/jdbc/JDBCUrl.java +++ b/src/main/java/org/mariadb/jdbc/JDBCUrl.java @@ -49,18 +49,22 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS package org.mariadb.jdbc; +import java.util.Properties; + public class JDBCUrl { private String username; private String password; private String database; + private Properties properties; private HostAddress addresses[]; - private JDBCUrl( String username, String password, String database, HostAddress addresses[]) { + private JDBCUrl( String username, String password, String database, HostAddress addresses[], Properties properties) { this.username = username; this.password = password; this.database = database; this.addresses = addresses; + this.properties = properties; } /* @@ -83,20 +87,22 @@ private static JDBCUrl parseConnectorJUrl(String url) { hostname = tokens[0]; database = (tokens.length > 1) ? tokens[1] : null; - + Properties properties = new Properties(); + if (database == null) { - return new JDBCUrl("", "", database, HostAddress.parse(hostname)); + return new JDBCUrl("", "", database, HostAddress.parse(hostname), properties); } //check if there are parameters - if (database.indexOf('?') > -1) - { + if (database.indexOf('?') > -1) { + String[] credentials = database.substring(database.indexOf('?') + 1, database.length()).split("&"); database = database.substring(0, database.indexOf('?')); - for (int i = 0; i < credentials.length; i++) - { + for (int i = 0; i < credentials.length; i++){ + String[] parameter = credentials[i].split("="); + properties.put(parameter[0], (parameter.length>1)?parameter[1]:""); if (credentials[i].startsWith("user=")) user=credentials[i].substring(5); else if (credentials[i].startsWith("password=")) @@ -104,7 +110,7 @@ else if (credentials[i].startsWith("password=")) } } - return new JDBCUrl(user, password, database, HostAddress.parse(hostname)); + return new JDBCUrl(user, password, database, HostAddress.parse(hostname), properties); } static boolean acceptsURL(String url) { @@ -127,6 +133,7 @@ public static JDBCUrl parse(final String url) { } return null; } + public String getUsername() { return username; } @@ -147,9 +154,12 @@ public String getDatabase() { return database; } - public HostAddress[] getHostAddresses() { - return this.addresses; + return this.addresses; + } + + public Properties getProperties() { + return properties; } public String toString() { diff --git a/src/main/java/org/mariadb/jdbc/MySQLConnection.java b/src/main/java/org/mariadb/jdbc/MySQLConnection.java index 471857f42..0215fc90e 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLConnection.java +++ b/src/main/java/org/mariadb/jdbc/MySQLConnection.java @@ -53,6 +53,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.common.QueryException; import org.mariadb.jdbc.internal.common.Utils; import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.Protocol; import java.net.SocketException; import java.sql.*; @@ -64,7 +65,7 @@ public final class MySQLConnection implements Connection { /** * the protocol to communicate with. */ - private final MySQLProtocol protocol; + private final Protocol protocol; /** * save point count - to generate good names for the savepoints. */ @@ -88,12 +89,12 @@ public final class MySQLConnection implements Connection { * * @param protocol the protocol to use. */ - private MySQLConnection(MySQLProtocol protocol) { + private MySQLConnection(Protocol protocol) { this.protocol = protocol; clientInfoProperties = protocol.getInfo(); } - MySQLProtocol getProtocol() { + Protocol getProtocol() { return protocol; } @@ -107,7 +108,7 @@ static TimeZone getTimeZone(String id) throws SQLException { return tz; } - public static MySQLConnection newConnection(MySQLProtocol protocol) throws SQLException { + public static MySQLConnection newConnection(Protocol protocol) throws SQLException { MySQLConnection connection = new MySQLConnection(protocol); Properties info = protocol.getInfo(); @@ -298,7 +299,7 @@ public void setReadOnly(final boolean readOnly) throws SQLException { * connection */ public boolean isReadOnly() throws SQLException { - return false; + return !protocol.checkIfMaster(); } public static String quoteIdentifier(String s) { @@ -459,7 +460,7 @@ public int getTransactionIsolation() throws SQLException { * @see java.sql.SQLWarning */ public SQLWarning getWarnings() throws SQLException { - if (warningsCleared || isClosed() || !protocol.hasWarnings) { + if (warningsCleared || isClosed() || !protocol.hasWarnings()) { return null; } Statement st = null; @@ -1287,11 +1288,6 @@ public String getPinGlobalTxToPhysicalConnection() { return protocol.getPinGlobalTxToPhysicalConnection(); } - - public void setHostFailed() { - protocol.setHostFailed(); - } - volatile int lowercaseTableNames = -1; public int getLowercaseTableNames() throws SQLException { if (lowercaseTableNames == -1) { diff --git a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java b/src/main/java/org/mariadb/jdbc/MySQLDataSource.java index 6dca50afb..d9f03e80d 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java +++ b/src/main/java/org/mariadb/jdbc/MySQLDataSource.java @@ -53,11 +53,12 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.SQLExceptionMapper; import org.mariadb.jdbc.internal.common.QueryException; import org.mariadb.jdbc.internal.common.Utils; -import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.*; import javax.sql.*; import java.io.PrintWriter; +import java.lang.reflect.Proxy; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; @@ -297,7 +298,13 @@ private void resetUrl() { public Connection getConnection() throws SQLException { createUrl(); try { - return MySQLConnection.newConnection(new MySQLProtocol(url, username, password, info)); + Protocol proxyfiedProtocol = (Protocol) Proxy.newProxyInstance( + MySQLProtocol.class.getClassLoader(), + new Class[]{Protocol.class}, + new FailoverProxy(new MySQLProtocol(url, username, password, info), new SingleHostListener()) + ); + proxyfiedProtocol.initializeConnection(); + return MySQLConnection.newConnection(proxyfiedProtocol); } catch (QueryException e) { SQLExceptionMapper.throwException(e, null, null); return null; @@ -317,7 +324,9 @@ public Connection getConnection(final String username, final String password) th createUrl(); try { Properties props = info == null ? new Properties() : info; - return MySQLConnection.newConnection(new MySQLProtocol(url, username, password, props)); + Protocol proxyfiedProtocol = ConnectorUtils.retrieveProxy(url, username, password, props); + proxyfiedProtocol.initializeConnection(); + return MySQLConnection.newConnection(proxyfiedProtocol); } catch (QueryException e) { SQLExceptionMapper.throwException(e, null, null); return null; diff --git a/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java b/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java index 1d3b0517b..d974e557a 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java +++ b/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java @@ -96,8 +96,8 @@ private String dataTypeClause (String fullTypeColumnName){ " WHEN 'binary' THEN " + Types.BINARY + " WHEN 'time' THEN " + Types.TIME + " WHEN 'timestamp' THEN " + Types.TIMESTAMP + - " WHEN 'tinyint' THEN " + (((connection.getProtocol().datatypeMappingFlags & MySQLValueObject.TINYINT1_IS_BIT)== 0)? Types.TINYINT : "IF(" + fullTypeColumnName + "='tinyint(1)'," + Types.BIT + "," + Types.TINYINT + ") ") + - " WHEN 'year' THEN " + (((connection.getProtocol().datatypeMappingFlags & MySQLValueObject.YEAR_IS_DATE_TYPE)== 0)? Types.SMALLINT :Types.DATE) + + " WHEN 'tinyint' THEN " + (((connection.getProtocol().getDatatypeMappingFlags() & MySQLValueObject.TINYINT1_IS_BIT)== 0)? Types.TINYINT : "IF(" + fullTypeColumnName + "='tinyint(1)'," + Types.BIT + "," + Types.TINYINT + ") ") + + " WHEN 'year' THEN " + (((connection.getProtocol().getDatatypeMappingFlags() & MySQLValueObject.YEAR_IS_DATE_TYPE)== 0)? Types.SMALLINT :Types.DATE) + " ELSE " + Types.OTHER + " END "; } @@ -1729,7 +1729,6 @@ public static ResultSet getImportedKeys(String tableDef, String tableName, Strin List data = new ArrayList(); for (String p:parts) { - //System.out.println("--" + p); p = p.trim(); if (!p.startsWith("CONSTRAINT") && !p.contains("FOREIGN KEY")) continue; diff --git a/src/main/java/org/mariadb/jdbc/MySQLResultSet.java b/src/main/java/org/mariadb/jdbc/MySQLResultSet.java index 5c487658a..d71760d21 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLResultSet.java +++ b/src/main/java/org/mariadb/jdbc/MySQLResultSet.java @@ -53,10 +53,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.common.QueryException; import org.mariadb.jdbc.internal.common.ValueObject; import org.mariadb.jdbc.internal.common.queryresults.*; -import org.mariadb.jdbc.internal.mysql.MySQLColumnInformation; -import org.mariadb.jdbc.internal.mysql.MySQLProtocol; -import org.mariadb.jdbc.internal.mysql.MySQLType; -import org.mariadb.jdbc.internal.mysql.MySQLValueObject; +import org.mariadb.jdbc.internal.mysql.*; import java.io.IOException; import java.io.InputStream; @@ -76,7 +73,7 @@ public class MySQLResultSet implements ResultSet { public static final MySQLResultSet EMPTY = createEmptyResultSet(); private QueryResult queryResult; private Statement statement; - private MySQLProtocol protocol; + private Protocol protocol; private boolean lastGetWasNull; private boolean warningsCleared; ColumnNameMap columnNameMap; @@ -84,7 +81,7 @@ public class MySQLResultSet implements ResultSet { protected MySQLResultSet() { } - public MySQLResultSet(QueryResult dqr, Statement statement, MySQLProtocol protocol, Calendar cal) { + public MySQLResultSet(QueryResult dqr, Statement statement, Protocol protocol, Calendar cal) { this.queryResult = dqr; this.statement = statement; this.protocol = protocol; @@ -412,7 +409,7 @@ public ResultSetMetaData getMetaData() throws SQLException { && "true".equalsIgnoreCase(protocol.getInfo().getProperty("useOldAliasMetadataBehavior"))) returnTableAlias = true; - return new MySQLResultSetMetaData(queryResult.getColumnInformation(), protocol.datatypeMappingFlags, returnTableAlias); + return new MySQLResultSetMetaData(queryResult.getColumnInformation(), protocol.getDatatypeMappingFlags(), returnTableAlias); } /** @@ -447,7 +444,7 @@ public ResultSetMetaData getMetaData() throws SQLException { */ public Object getObject(int columnIndex) throws SQLException { try { - return getValueObject(columnIndex).getObject(protocol.datatypeMappingFlags, cal); + return getValueObject(columnIndex).getObject(protocol.getDatatypeMappingFlags(), cal); } catch (ParseException e) { throw SQLExceptionMapper.getSQLException("Could not get object: " + e.getMessage(), "S1009", e); } @@ -3753,7 +3750,7 @@ public T getObject(String arg0, Class arg1) throws SQLException { * @param findColumnReturnsOne - special parameter, used only in generated key result sets */ static ResultSet createResultSet(String[] columnNames, MySQLType[] columnTypes, String[][] data, - MySQLProtocol protocol, boolean findColumnReturnsOne) { + Protocol protocol, boolean findColumnReturnsOne) { int N = columnNames.length; MySQLColumnInformation[] columns = new MySQLColumnInformation[N]; @@ -3810,7 +3807,7 @@ public int findColumn(String name) { * @param protocol */ static ResultSet createResultSet(String[] columnNames, MySQLType[] columnTypes, String[][] data, - MySQLProtocol protocol) { + Protocol protocol) { return createResultSet(columnNames, columnTypes, data, protocol,false); } @@ -3825,7 +3822,7 @@ static ResultSet createResultSet(String[] columnNames, MySQLType[] columnTypes, * @param findColumnReturnsOne - special parameter, used only in generated key result sets */ static ResultSet createResultSet(MySQLColumnInformation[] columns, String[][] data, - MySQLProtocol protocol, boolean findColumnReturnsOne) { + Protocol protocol, boolean findColumnReturnsOne) { int N = columns.length; byte[] BOOL_TRUE = {1}; @@ -3875,7 +3872,7 @@ public int findColumn(String name) { * that are represented as "1" or "0" strings * @param protocol */ - static ResultSet createResultSet(MySQLColumnInformation[] columns, String[][] data, MySQLProtocol protocol) { + static ResultSet createResultSet(MySQLColumnInformation[] columns, String[][] data, Protocol protocol) { return createResultSet(columns, data, protocol, false); } diff --git a/src/main/java/org/mariadb/jdbc/MySQLServerSidePreparedStatement.java b/src/main/java/org/mariadb/jdbc/MySQLServerSidePreparedStatement.java index 1f43d4799..b83f7686f 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLServerSidePreparedStatement.java +++ b/src/main/java/org/mariadb/jdbc/MySQLServerSidePreparedStatement.java @@ -4,6 +4,7 @@ import org.mariadb.jdbc.internal.common.QueryException; import org.mariadb.jdbc.internal.mysql.MySQLColumnInformation; import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.Protocol; import java.io.InputStream; import java.io.Reader; @@ -27,7 +28,7 @@ public class MySQLServerSidePreparedStatement implements PreparedStatement { private void prepare(String sql) throws SQLException { try { - MySQLProtocol protocol = connection.getProtocol(); + Protocol protocol = connection.getProtocol(); MySQLProtocol.PrepareResult result; synchronized (protocol) { if (protocol.hasUnreadData()) { @@ -44,7 +45,7 @@ private void prepare(String sql) throws SQLException { returnTableAlias = true; metadata = new MySQLResultSetMetaData(result.columns, - protocol.datatypeMappingFlags, returnTableAlias); + protocol.getDatatypeMappingFlags(), returnTableAlias); parameterInfo = result.parameters; statementId = result.statementId; } catch (QueryException e) { diff --git a/src/main/java/org/mariadb/jdbc/MySQLStatement.java b/src/main/java/org/mariadb/jdbc/MySQLStatement.java index bc8e4a5b6..c9edd40dd 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLStatement.java +++ b/src/main/java/org/mariadb/jdbc/MySQLStatement.java @@ -58,6 +58,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.common.queryresults.QueryResult; import org.mariadb.jdbc.internal.common.queryresults.ResultSetType; import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.Protocol; import java.io.IOException; import java.io.InputStream; @@ -69,7 +70,7 @@ public class MySQLStatement implements Statement { /** * the protocol used to talk to the server. */ - private final MySQLProtocol protocol; + private final Protocol protocol; /** * the Connection object. */ @@ -125,7 +126,7 @@ public MySQLStatement(MySQLConnection connection) { * * @return the protocol used. */ - public MySQLProtocol getProtocol() { + public Protocol getProtocol() { return protocol; } @@ -158,37 +159,20 @@ public void run() { getTimer().schedule(timerTask, queryTimeout*1000); } - // Part of query prolog - check if connection is broken and reconnect - private void checkReconnect() throws SQLException { - if (protocol.shouldReconnect()) { - try { - protocol.connect(); - } catch (QueryException qe) { - SQLExceptionMapper.throwException(qe, connection, this); - } - } else if (protocol.shouldTryFailback()) { - try { - protocol.reconnectToMaster(); - } catch (Exception e) { - // Do nothing - } - } - } - void executeQueryProlog() throws SQLException{ if (isClosed()) { throw new SQLException("execute() is called on closed statement"); } - checkReconnect(); + if (protocol.isClosed()){ - throw new SQLException("execute() is called on closed connection"); + throw new SQLException("execute() is called on closed connection"); } if (protocol.hasUnreadData()) { throw new SQLException("There is an open result set on the current connection, "+ "which must be closed prior to executing a query"); } if (protocol.hasMoreResults()) { - // Skip remaining result sets. CallableStatement might return many of them - + // Skip remaining result sets. CallableStatement might return many of them - // not only the "select" result sets, but also the "update" results while(getMoreResults(true)) { } @@ -197,13 +181,13 @@ void executeQueryProlog() throws SQLException{ cachedResultSets.clear(); MySQLConnection conn = (MySQLConnection)getConnection(); conn.reenableWarnings(); - + try { protocol.setMaxRows(maxRows); } catch(QueryException qe) { SQLExceptionMapper.throwException(qe, connection, this); } - + if (queryTimeout != 0) { setTimerTask(); } @@ -276,10 +260,9 @@ private void executeQueryEpilog(QueryException e, Query query) throws SQLExcepti * @throws SQLException */ protected boolean execute(Query query) throws SQLException { - //System.out.println(query); synchronized (protocol) { - if (protocol.activeResult != null) { - protocol.activeResult.close(); + if (protocol.getActiveResult() != null) { + protocol.getActiveResult().close(); } executing = true; QueryException exception = null; diff --git a/src/main/java/org/mariadb/jdbc/MySQLXAResource.java b/src/main/java/org/mariadb/jdbc/MySQLXAResource.java index 899d56eec..ec4640639 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLXAResource.java +++ b/src/main/java/org/mariadb/jdbc/MySQLXAResource.java @@ -44,7 +44,6 @@ XAException mapXAException(SQLException sqle) { } void execute(String command) throws XAException { - //System.out.println(command); try { connection.createStatement().execute(command); }catch (SQLException sqle) { diff --git a/src/main/java/org/mariadb/jdbc/internal/SQLExceptionMapper.java b/src/main/java/org/mariadb/jdbc/internal/SQLExceptionMapper.java index a2f402c00..8c7f514dd 100644 --- a/src/main/java/org/mariadb/jdbc/internal/SQLExceptionMapper.java +++ b/src/main/java/org/mariadb/jdbc/internal/SQLExceptionMapper.java @@ -108,7 +108,6 @@ public static void throwException(QueryException e, MySQLConnection connection, SQLStates state = SQLStates.fromString(sqlState); if (connection != null) { if (state.equals(SQLStates.CONNECTION_EXCEPTION)) { - connection.setHostFailed(); if (connection.pooledConnection != null) { connection.pooledConnection.fireConnectionErrorOccured(sqlException); } @@ -119,6 +118,7 @@ else if(connection.pooledConnection!= null && statement != null) { } throw sqlException; } + private static SQLException get(final QueryException e) { final String sqlState = e.getSqlState(); final SQLStates state = SQLStates.fromString(sqlState); diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraHostListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraHostListener.java new file mode 100644 index 000000000..446e090e7 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraHostListener.java @@ -0,0 +1,117 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.internal.common.QueryException; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Logger; + +public class AuroraHostListener extends MultiHostListener { + private final static Logger log = Logger.getLogger(AuroraHostListener.class.getName()); + + public AuroraHostListener() { + masterProtocol = null; + secondaryProtocol = null; + } + + public void initializeConnection() throws QueryException, SQLException { + this.masterProtocol = (MultiNodesProtocol)this.proxy.currentProtocol; + this.secondaryProtocol = new MultiNodesProtocol(this.masterProtocol.jdbcUrl, + this.masterProtocol.getUsername(), + this.masterProtocol.getPassword(), + this.masterProtocol.getInfo()); + //set failover data to force connection + proxy.masterHostFailTimestamp = System.currentTimeMillis(); + proxy.secondaryHostFailTimestamp = System.currentTimeMillis(); + //TODO for perf : initial masterPrococol load and replace by connected one -> can be better + + launchSearchLoopConnection(); + } + + public void launchSearchLoopConnection() throws QueryException, SQLException { + proxy.currentConnectionAttempts++; + proxy.lastRetry = System.currentTimeMillis(); + + if (proxy.currentConnectionAttempts >= proxy.retriesAllDown) { + throw new QueryException("Too many reconnection attempts ("+proxy.retriesAllDown+")"); + } + + AuroraMultiNodesProtocol newProtocol = new AuroraMultiNodesProtocol(this.masterProtocol.jdbcUrl, + this.masterProtocol.getUsername(), + this.masterProtocol.getPassword(), + this.masterProtocol.getInfo()); + List loopAddress = Arrays.asList(this.masterProtocol.jdbcUrl.getHostAddresses().clone()); + List failAddress = new ArrayList(); + + boolean searchForMaster = false; + boolean searchForSecondary = false; + + if (proxy.masterHostFailTimestamp == 0) { + loopAddress.remove(masterProtocol.currentHost); + failAddress.add(masterProtocol.currentHost); + } else searchForMaster = true; + + if (proxy.secondaryHostFailTimestamp == 0) { + loopAddress.remove(secondaryProtocol.currentHost); + failAddress.add(secondaryProtocol.currentHost); + } else searchForSecondary = true; + + if ((searchForMaster || searchForSecondary) && isLooping.compareAndSet(false, true)) { + newProtocol.loop(this, loopAddress, failAddress, searchForMaster, searchForSecondary); + } + } + + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraMultiNodesProtocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraMultiNodesProtocol.java new file mode 100644 index 000000000..199fedc94 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraMultiNodesProtocol.java @@ -0,0 +1,149 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.query.MySQLQuery; +import org.mariadb.jdbc.internal.common.queryresults.SelectQueryResult; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class AuroraMultiNodesProtocol extends MultiNodesProtocol { + + public AuroraMultiNodesProtocol(JDBCUrl url, + final String username, + final String password, + Properties info) { + super(url, username, password, info); + } + + public boolean checkIfMaster() throws SQLException { + try { + + SelectQueryResult queryResult = (SelectQueryResult) executeQuery(new MySQLQuery("show global variables like 'innodb_read_only'")); + if (queryResult != null) { + queryResult.next(); + masterConnection = "OFF".equals(queryResult.getValueObject(1).getString()); + } else { + masterConnection = false; + } + return masterConnection; + + } catch(IOException ioe) { + throw new SQLException(ioe); + } catch (QueryException qe) { + throw new SQLException(qe); + } + } + + + public void loop(AuroraHostListener listener, List addrs, List failAddress, boolean searchForMaster, boolean searchForSecondary) throws QueryException { + + for(HostAddress host : addrs) { + try { + currentHost = host; + connect(currentHost.host, currentHost.port); + if (searchForMaster && isMasterConnection()) { + searchForMaster = false; + listener.foundActiveMaster(this); + if (!searchForSecondary) return; + else { + loopInternal(listener, addrs, failAddress, currentHost, searchForMaster, searchForSecondary); + return; + } + } + if (searchForSecondary && !isMasterConnection()) { + searchForSecondary = false; + listener.foundActiveSecondary(this); + if (!searchForMaster) return; + else { + loopInternal(listener, addrs, failAddress, currentHost, searchForMaster, searchForSecondary); + return; + } + } + + } catch (QueryException e ) { + log.finest("Could not connect to " + host +" : " + e.getMessage()); + } catch (SQLException e ) { + log.finest("Could not connect to " + host +" : " + e.getMessage()); + } catch (IOException e ) { + log.finest("Could not connect to " + host +" : " + e.getMessage()); + } + if (!searchForMaster && !searchForSecondary) return; + } + + + if ((searchForMaster || searchForSecondary) && failAddress != null) { + //the loop has trait all host but failed and not found what we want, so continue on failed Hosts + loopInternal(listener, failAddress, null, null, searchForMaster, searchForSecondary); + } + + if (searchForMaster || searchForSecondary) { + throw new QueryException("No active connection found"); + } + } + + + private void loopInternal(AuroraHostListener listener, List addrs, List failAddress, HostAddress hostToRemove, boolean searchForMaster, boolean searchForSecondary) throws QueryException { + AuroraMultiNodesProtocol newProtocol = new AuroraMultiNodesProtocol(this.jdbcUrl, + this.getUsername(), + this.getPassword(), + this.getInfo()); + List remainingAddrs = new ArrayList(addrs); + if (hostToRemove != null) remainingAddrs.remove(hostToRemove); + newProtocol.loop(listener, remainingAddrs, failAddress, searchForMaster, searchForSecondary); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/ConnectorUtils.java b/src/main/java/org/mariadb/jdbc/internal/mysql/ConnectorUtils.java new file mode 100644 index 000000000..308a13922 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/ConnectorUtils.java @@ -0,0 +1,41 @@ +package org.mariadb.jdbc.internal.mysql; + +import org.mariadb.jdbc.JDBCUrl; + +import java.lang.reflect.Proxy; +import java.util.Properties; + +/** + * Created by diego_000 on 17/05/2015. + */ +public class ConnectorUtils { + public static Protocol retrieveProxy(JDBCUrl jdbcUrl, + final String username, + final String password, + Properties info) { + Protocol proxyfiedProtocol; + if (jdbcUrl.getHostAddresses().length == 1) { + proxyfiedProtocol = (Protocol) Proxy.newProxyInstance( + MySQLProtocol.class.getClassLoader(), + new Class[]{Protocol.class}, + new FailoverProxy(new MySQLProtocol(jdbcUrl, username, password, info), new SingleHostListener())); + } else { + String aurora = info.getProperty("aurora", "false"); + boolean auroraMultinode = "true".equals(aurora) || "1".equals(aurora); + if (auroraMultinode) { + proxyfiedProtocol = (Protocol) Proxy.newProxyInstance( + AuroraMultiNodesProtocol.class.getClassLoader(), + new Class[] {Protocol.class}, + new FailoverProxy(new AuroraMultiNodesProtocol(jdbcUrl, username, password, info), new AuroraHostListener())); + } else { + proxyfiedProtocol = (Protocol) Proxy.newProxyInstance( + MultiNodesProtocol.class.getClassLoader(), + new Class[] {Protocol.class}, + new FailoverProxy(new MultiNodesProtocol(jdbcUrl, username, password, info), new MultiHostListener())); + + } + } + return proxyfiedProtocol; + } + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverListener.java new file mode 100644 index 000000000..f93f5af1e --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverListener.java @@ -0,0 +1,65 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.internal.common.QueryException; + +import java.lang.reflect.Method; +import java.sql.SQLException; + +public interface FailoverListener { + void setProxy(FailoverProxy proxy); + void initializeConnection() throws QueryException, SQLException; + void preExecute() throws SQLException; + void postClose() throws SQLException; + void switchReadOnlyConnection(Boolean readonly) throws QueryException, SQLException ; + HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable; + HandleErrorResult secondaryFail(Method method, Object[] args) throws Throwable; +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverProxy.java b/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverProxy.java new file mode 100644 index 000000000..9523a8efa --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverProxy.java @@ -0,0 +1,246 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.internal.common.QueryException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +public class FailoverProxy implements InvocationHandler { + private final static Logger log = Logger.getLogger(FailoverProxy.class.getName()); + + + /* =========================== Failover parameters ========================================= */ + /** + * Driver must recreateConnection after a failover + */ + protected boolean autoReconnect = false; + protected boolean autoReconnectForPools = false; + + /** + * If autoReconnect is enabled, the initial time to wait between re-connect attempts (in seconds, defaults to 2) + */ + protected int initialTimeout = 2; + + /** + * Maximum number of reconnects to attempt if autoReconnect is true, default is 3 + */ + protected int maxReconnects=3; + + + /** + * Number of seconds to issue before falling back to master when failed over (when using multi-host failover). + * Whichever condition is met first, 'queriesBeforeRetryMaster' or 'secondsBeforeRetryMaster' will cause an + * attempt to be made to reconnect to the master. Defaults to 50 + */ + protected int secondsBeforeRetryMaster = 50; + + /** + * Number of queries to issue before falling back to master when failed over (when using multi-host failover). + * Whichever condition is met first, 'queriesBeforeRetryMaster' or 'secondsBeforeRetryMaster' will cause an + * attempt to be made to reconnect to the master. Defaults to 30 + */ + protected int queriesBeforeRetryMaster = 30; + + /** + * When using loadbalancing, the number of times the driver should cycle through available hosts, attempting to connect. + * Between cycles, the driver will pause for 250ms if no servers are available. + */ + protected int retriesAllDown = 120; + protected long lastRetry = 0; + + protected int queriesSinceFailover=0; + protected long secondaryHostFailTimestamp = 0; + + + /* =========================== Failover variables ========================================= */ + protected long masterHostFailTimestamp = 0; + protected int currentConnectionAttempts = 0; + + protected AtomicBoolean currentReadOnlyAsked=new AtomicBoolean(); + protected Protocol currentProtocol; + + private FailoverListener listener; + + public FailoverProxy(Protocol protocol, FailoverListener listener) { + this.currentProtocol = protocol; + this.listener = listener; + this.listener.setProxy(this); + parseHAOptions(); + } + + + protected void parseHAOptions() { + String s = currentProtocol.getInfo().getProperty("autoReconnect"); + if (s != null && s.equals("true")) autoReconnect = true; + + s = currentProtocol.getInfo().getProperty("autoReconnectForPools"); + if (s != null && s.equals("true")) autoReconnectForPools = true; + + s = currentProtocol.getInfo().getProperty("maxReconnects"); + if (s != null) maxReconnects = Integer.parseInt(s); + + s = currentProtocol.getInfo().getProperty("queriesBeforeRetryMaster"); + if (s != null) queriesBeforeRetryMaster = Integer.parseInt(s); + + s = currentProtocol.getInfo().getProperty("secondsBeforeRetryMaster"); + if (s != null) secondsBeforeRetryMaster = Integer.parseInt(s); + + s = currentProtocol.getInfo().getProperty("retriesAllDown"); + if (s != null) retriesAllDown = Integer.parseInt(s); + } + + + /** + * 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(); + + if ("initializeConnection".equals(methodName)) { + this.listener.initializeConnection(); + } + + if ("executeQuery".equals(methodName)) { + if (masterHostFailTimestamp!=0)queriesSinceFailover++; + this.listener.preExecute(); + } + if ("setReadonly".equals(methodName)) { + this.listener.switchReadOnlyConnection((Boolean) args[0]); + } + + try { + Object returnObj = method.invoke(currentProtocol, args); + if ("close".equals(methodName)) { + this.listener.postClose(); + } + return returnObj; + } catch (InvocationTargetException e) { + if (e.getTargetException() != null) { + if (e.getTargetException() instanceof QueryException) { + if (hasToHandleFailover((QueryException) e.getTargetException())) { + HandleErrorResult handleErrorResult = handleFailover(method, args); + if (handleErrorResult.mustThrowError) throw e.getTargetException(); + return handleErrorResult.resultObject; + } + } + throw e.getTargetException(); + } + throw e; + } + } + + private HandleErrorResult handleFailover(Method method, Object[] args) throws Throwable { + if (currentProtocol.isMasterConnection()) { + //trying to connect of first error + if (masterHostFailTimestamp == 0) { + masterHostFailTimestamp = System.currentTimeMillis(); + currentConnectionAttempts = 0; + return listener.primaryFail(method, args); + } + //if not first error, just launched error + return new HandleErrorResult(); + } else { + if (secondaryHostFailTimestamp == 0) { + secondaryHostFailTimestamp = System.currentTimeMillis(); + currentConnectionAttempts = 0; + return listener.secondaryFail(method, args); + } + return new HandleErrorResult(); + } + } + + /** + * 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 + * + * @param e the Exception + * @return true if there has been a connection error that must be handled by failover + */ + public boolean hasToHandleFailover(QueryException e){ + if (e.getSqlState() != null && e.getSqlState().startsWith("08")) { + return true; + } + return false; + } + + protected void resetMasterFailoverData() { + currentConnectionAttempts = 0; + masterHostFailTimestamp = 0; + lastRetry = 0; + } + + protected void resetSecondaryFailoverData() { + currentConnectionAttempts = 0; + secondaryHostFailTimestamp = 0; + lastRetry = 0; + } + + + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/HandleErrorResult.java b/src/main/java/org/mariadb/jdbc/internal/mysql/HandleErrorResult.java new file mode 100644 index 000000000..db7163546 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/HandleErrorResult.java @@ -0,0 +1,55 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +public class HandleErrorResult { + public boolean mustThrowError = true; + public Object resultObject = null; +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/MultiHostListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/MultiHostListener.java new file mode 100644 index 000000000..ee2608977 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/MultiHostListener.java @@ -0,0 +1,354 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.query.Query; + +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Logger; + +public class MultiHostListener implements FailoverListener { + private final static Logger log = Logger.getLogger(MultiHostListener.class.getName()); + + protected FailoverProxy proxy; + protected MultiNodesProtocol masterProtocol; + protected MultiNodesProtocol secondaryProtocol; + ScheduledExecutorService exec = Executors.newSingleThreadScheduledExecutor(); + protected AtomicBoolean isLooping = new AtomicBoolean(); + protected AtomicBoolean isLoopingMaster = new AtomicBoolean(); + protected AtomicBoolean isLoopingSecondary = new AtomicBoolean(); + + public MultiHostListener() { + masterProtocol = null; + secondaryProtocol = null; + } + + public void setProxy(FailoverProxy proxy) { + this.proxy = proxy; + } + + + public void initializeConnection() throws QueryException, SQLException { + this.masterProtocol = (MultiNodesProtocol)this.proxy.currentProtocol; + this.secondaryProtocol = new MultiNodesProtocol(this.masterProtocol.jdbcUrl, + this.masterProtocol.getUsername(), + this.masterProtocol.getPassword(), + this.masterProtocol.getInfo()); + //set failover data to force connection + proxy.masterHostFailTimestamp = System.currentTimeMillis(); + proxy.secondaryHostFailTimestamp = System.currentTimeMillis(); + //TODO for perf : initial masterPrococol load and replace by connected one -> can be better + + launchSearchLoopConnection(); + } + + + public void postClose() throws SQLException { + if (!this.masterProtocol.isClosed()) this.masterProtocol.close(); + if (!this.secondaryProtocol.isClosed()) this.secondaryProtocol.close(); + } + + @Override + public void preExecute() throws SQLException { + if (shouldReconnect()) { + launchAsyncSearchLoopConnection(); + } + } + + private boolean shouldReconnect() { + if (proxy.currentProtocol.inTransaction()) return false; + if (proxy.currentConnectionAttempts > proxy.retriesAllDown) return false; + long now = System.currentTimeMillis(); + + if (proxy.masterHostFailTimestamp !=0) { + if ((now - proxy.masterHostFailTimestamp) / 1000 > proxy.secondsBeforeRetryMaster) return true; + if (proxy.queriesSinceFailover > proxy.queriesBeforeRetryMaster) return true; + } + + if (proxy.secondaryHostFailTimestamp !=0) { + if ((now - proxy.secondaryHostFailTimestamp) / 1000 > proxy.secondsBeforeRetryMaster) return true; + if (proxy.queriesSinceFailover > proxy.queriesBeforeRetryMaster) return true; + } + + return false; + } + + public void launchAsyncSearchLoopConnection() { + final MultiHostListener hostListener = MultiHostListener.this; + Executors.newSingleThreadExecutor().execute(new Runnable() { + public void run() { + try { + hostListener.launchSearchLoopConnection(); + } catch (Exception e) { } + } + }); + } + + public void launchSearchLoopConnection() throws QueryException, SQLException { + MultiNodesProtocol newProtocol = new MultiNodesProtocol(this.masterProtocol.jdbcUrl, + this.masterProtocol.getUsername(), + this.masterProtocol.getPassword(), + this.masterProtocol.getInfo()); + proxy.currentConnectionAttempts++; + proxy.lastRetry = System.currentTimeMillis(); + + if (proxy.currentConnectionAttempts >= proxy.retriesAllDown) { + throw new QueryException("Too many reconnection attempts ("+proxy.retriesAllDown+")"); + } + + boolean searchForMaster = (proxy.masterHostFailTimestamp > 0); + boolean searchForSecondary = (proxy.secondaryHostFailTimestamp > 0); + + List loopSecondaryAddresses = new LinkedList(Arrays.asList(this.masterProtocol.jdbcUrl.getHostAddresses().clone())); + loopSecondaryAddresses.remove(this.masterProtocol.jdbcUrl.getHostAddresses()[0]); + + QueryException queryException = null; + SQLException sqlException = null; + if (searchForMaster && isLoopingMaster.compareAndSet(false, true)) { + try { + newProtocol.connectMaster(this); + isLoopingMaster.set(false); + } catch (QueryException e1) { + queryException = e1; + } catch (SQLException e1) { + sqlException = e1; + } + } + + if (searchForSecondary && isLoopingSecondary.compareAndSet(false, true)) { + try { + newProtocol.connectSecondary(this, loopSecondaryAddresses); + isLoopingSecondary.set(false); + } catch (QueryException e1) { + if (queryException == null) queryException = e1; + } catch (SQLException e1) { + if (sqlException == null) sqlException = e1; + } + } + if (sqlException != null ) throw sqlException; + if (queryException != null ) throw queryException; + } + + + public synchronized void foundActiveMaster(MultiNodesProtocol newMasterProtocol) { + log.fine("found active master connection"); + this.masterProtocol = newMasterProtocol; + if (!proxy.currentReadOnlyAsked.get()) { + //actually on a secondary read-only because master was unknown. + //So select master as currentConnection + try { + syncConnection(proxy.currentProtocol, this.masterProtocol); + } catch (Exception e) { + log.fine("Some error append during connection parameter synchronisation : " + e.getMessage()); + } + log.finest("switching to master connection"); + proxy.currentProtocol = this.masterProtocol; + } + proxy.resetMasterFailoverData(); + } + + + public synchronized void foundActiveSecondary(MultiNodesProtocol newSecondaryProtocol) { + log.fine("found active secondary connection"); + this.secondaryProtocol = newSecondaryProtocol; + if (proxy.currentReadOnlyAsked.get() && proxy.currentProtocol.isMasterConnection()) { + //actually on a master since the failover, so switching to a read-only connection as asked + try { + syncConnection(proxy.currentProtocol, this.secondaryProtocol); + } catch (Exception e) { + log.fine("Some error append during connection parameter synchronisation : " + e.getMessage()); + } + log.finest("switching to secondary connection"); + proxy.currentProtocol = this.secondaryProtocol; + } + proxy.resetSecondaryFailoverData(); + } + + @Override + public void switchReadOnlyConnection(Boolean mustBeRealOnly) throws QueryException, SQLException { + log.finest("switching to mustBeRealOnly = " + mustBeRealOnly + " mode"); + proxy.currentReadOnlyAsked.set(mustBeRealOnly); + if (proxy.currentReadOnlyAsked.get()) { + if (proxy.currentProtocol.isMasterConnection()) { + //must change to replica connection + if (proxy.secondaryHostFailTimestamp == 0) { + synchronized (this) { + log.finest("switching to secondary connection"); + syncConnection(this.masterProtocol, this.secondaryProtocol); + proxy.currentProtocol = this.secondaryProtocol; + } + } + } + } else { + if (!proxy.currentProtocol.isMasterConnection()) { + //must change to master connection + if (proxy.masterHostFailTimestamp == 0) { + synchronized (this) { + log.finest("switching to master connection"); + syncConnection(this.secondaryProtocol, this.masterProtocol); + proxy.currentProtocol = this.masterProtocol; + } + } else { + if (proxy.autoReconnect) { + try { + launchSearchLoopConnection(); + //connection established, no need to send Exception ! + return; + } catch (Exception e) { } + } + + if (isLooping.compareAndSet(false, true)) { + exec.scheduleAtFixedRate(new failLoop(this), 250, 250, TimeUnit.MILLISECONDS); + } + throw new QueryException("No primary host is actually connected"); + } + } + } + } + + private void syncConnection(Protocol from, Protocol to) throws QueryException, SQLException { + to.setMaxAllowedPacket(from.getMaxAllowedPacket()); + to.setMaxRows(from.getMaxRows()); + if (from.getDatabase() != null && !"".equals(from.getDatabase())) { + to.selectDB(to.getDatabase()); + } + + //TODO check if must handle transaction && autocommit ? + + } + + public synchronized HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable { + log.warning("SQL Primary node [" + this.masterProtocol.currentHost + "] connection fail"); + HandleErrorResult handleErrorResult = new HandleErrorResult(); + if (proxy.autoReconnect) { + try { + launchSearchLoopConnection(); + //connection established ! + handleErrorResult.resultObject = method.invoke(proxy.currentProtocol, args); + handleErrorResult.mustThrowError = false; + return handleErrorResult; + } catch (Exception e) { + if (isLooping.compareAndSet(false, true)) { + exec.scheduleAtFixedRate(new failLoop(this), 250, 250, TimeUnit.MILLISECONDS); + } + } + } + + //in multiHost, switch to secondary + log.finest("switching to secondary connection"); + syncConnection(this.masterProtocol, this.secondaryProtocol); + proxy.currentProtocol = this.secondaryProtocol; + + //loop reconnection if not already launched + if (isLooping.compareAndSet(false, true)) { + exec.scheduleAtFixedRate(new failLoop(this), 0, 250, TimeUnit.MILLISECONDS); + } + return handleErrorResult; + } + + + public synchronized HandleErrorResult secondaryFail(Method method, Object[] args) throws Throwable { + HandleErrorResult handleErrorResult = new HandleErrorResult(); + + //in multiHost, switch temporary to Master + log.finest("switching to master connection"); + syncConnection(this.secondaryProtocol, this.masterProtocol); + proxy.currentProtocol = this.masterProtocol; + + //now that we are on master, relaunched result if the result was not crashing the master + if (!"executeQuery".equals(method.getName()) && !"ALTER SYSTEM CRASH".equalsIgnoreCase(((Query)args[0]).getQuery())) { + handleErrorResult.resultObject = method.invoke(proxy.currentProtocol, args); + handleErrorResult.mustThrowError = false; + } + + //launch reconnection loop + if (isLooping.compareAndSet(false, true)) { + exec.scheduleAtFixedRate(new failLoop(this), 0, 250, TimeUnit.MILLISECONDS); + } + + return handleErrorResult; + } + + protected void stopFailover() { + exec.shutdown(); + isLooping.set(false); + } + + private class failLoop implements Runnable { + MultiHostListener listener; + public failLoop(MultiHostListener listener) { + this.listener = listener; + } + + public void run() { + if (listener.shouldReconnect()) { + try { + listener.launchSearchLoopConnection(); + //reconnection done ! + listener.stopFailover(); + } catch (Exception e) { + //do nothing + } + } else { + if (proxy.currentConnectionAttempts > proxy.retriesAllDown) listener.stopFailover(); + } + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/MultiNodesProtocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/MultiNodesProtocol.java new file mode 100644 index 000000000..ad34c570f --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/MultiNodesProtocol.java @@ -0,0 +1,157 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.SQLExceptionMapper; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.packet.ErrorPacket; +import org.mariadb.jdbc.internal.common.packet.PacketOutputStream; +import org.mariadb.jdbc.internal.common.packet.SyncPacketFetcher; +import org.mariadb.jdbc.internal.common.query.MySQLQuery; +import org.mariadb.jdbc.internal.common.queryresults.QueryResult; +import org.mariadb.jdbc.internal.common.queryresults.ResultSetType; +import org.mariadb.jdbc.internal.common.queryresults.SelectQueryResult; + +import java.io.IOException; +import java.net.Socket; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +public class MultiNodesProtocol extends MySQLProtocol { + + boolean masterConnection = false; + + public MultiNodesProtocol(JDBCUrl url, + final String username, + final String password, + Properties info) { + super(url, username, password, info); + } + + @Override + public void connect() throws QueryException, SQLException { + if (!isClosed()) { + close(); + } + + // There could be several addresses given in the URL spec, try all of them, and throw exception if all hosts + // fail. + HostAddress[] addrs = this.jdbcUrl.getHostAddresses(); + for(int i = 0; i < addrs.length; i++) { + currentHost = addrs[i]; + try { + connect(currentHost.host, currentHost.port); + return; + } catch (IOException e) { + if (i == addrs.length - 1) { + throw new QueryException("Could not connect to " + HostAddress.toString(addrs) + + " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } + } + } + } + + + + public void connectMaster(MultiHostListener listener) throws QueryException, SQLException { + //Master is considered the firstOne + HostAddress host = jdbcUrl.getHostAddresses()[0]; + try { + connect(currentHost.host, currentHost.port); + currentHost = host; + listener.foundActiveMaster(this); + } catch (IOException e) { + throw new QueryException("Could not connect to " + host + " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } + } + + public void connectSecondary(MultiHostListener listener, List secondaryAddresses) throws QueryException, SQLException { + for(int i = 0; i < secondaryAddresses.size(); i++) { + try { + currentHost = secondaryAddresses.get(i); + connect(currentHost.host, currentHost.port); + listener.foundActiveSecondary(this); + return; + } catch (IOException e) { + if (i == secondaryAddresses.size() - 1) { + throw new QueryException("Could not connect to " + HostAddress.toString(secondaryAddresses) + + " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } + } + } + + } + + + public boolean checkIfMaster() throws SQLException { + masterConnection = currentHost == jdbcUrl.getHostAddresses()[0]; + return masterConnection; + } + + + public boolean isMasterConnection() { + return masterConnection; + } + + @Override + public boolean createDB() { + if (masterConnection) { + String alias = info.getProperty("createDatabaseIfNotExist"); + return info != null + && (info.getProperty("createDB", "").equalsIgnoreCase("true") + || (alias != null && alias.equalsIgnoreCase("true"))); + } + return false; + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java index 6a2cfbb17..717bad4fe 100644 --- a/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java @@ -150,19 +150,19 @@ public X509Certificate[] getAcceptedIssuers() { } } -public class MySQLProtocol { - private final static Logger log = Logger.getLogger(MySQLProtocol.class.getName()); +public class MySQLProtocol implements Protocol { + protected final static Logger log = Logger.getLogger(MySQLProtocol.class.getName()); private boolean connected = false; - private Socket socket; - private PacketOutputStream writer; + protected Socket socket; + protected PacketOutputStream writer; private String version; private boolean readOnly = false; private String database; private final String username; private final String password; private int maxRows; /* max rows returned by a statement */ - private SyncPacketFetcher packetFetcher; - private final Properties info; + protected SyncPacketFetcher packetFetcher; + protected final Properties info; private long serverThreadId; public boolean moreResults = false; public boolean hasWarnings = false; @@ -176,51 +176,11 @@ public class MySQLProtocol { private int minorVersion; private int patchVersion; - boolean hostFailed; - long failTimestamp; - int reconnectCount; - int queriesSinceFailover; private int maxAllowedPacket; private byte serverLanguage; /* =========================== HA parameters ========================================= */ - /** - * Should the driver try to re-establish stale and/or dead connections? - * NOTE: exceptions will still be thrown, yet the next retry will repair the connection - */ - private boolean autoReconnect = false; - - /** - * Maximum number of reconnects to attempt if autoReconnect is true, default is 3 - */ - private int maxReconnects=3; - /** - * When using loadbalancing, the number of times the driver should cycle through available hosts, attempting to connect. - * Between cycles, the driver will pause for 250ms if no servers are available. 120 - */ - int retriesAllDown = 120; - /** - * If autoReconnect is enabled, the initial time to wait between re-connect attempts (in seconds, defaults to 2) - */ - int initialTimeout = 2; - /** - * When autoReconnect is enabled, and failoverReadonly is false, should we pick hosts to connect to on a round-robin - * basis? - */ - - boolean roundRobinLoadBalance = false; - /** - * Number of queries to issue before falling back to master when failed over (when using multi-host failover). - * Whichever condition is met first, 'queriesBeforeRetryMaster' or 'secondsBeforeRetryMaster' will cause an - * attempt to be made to reconnect to the master. Defaults to 50 - */ - int queriesBeforeRetryMaster = 50; - - /** - * How long should the driver wait, when failed over, before attempting 30 - */ - int secondsBeforeRetryMaster = 30; private InputStream localInfileInputStream; private SSLSocketFactory getSSLSocketFactory(boolean trustServerCertificate) throws QueryException @@ -250,11 +210,11 @@ private SSLSocketFactory getSSLSocketFactory(boolean trustServerCertificate) th * if there is a problem reading / sending the packets * @throws SQLException */ + public MySQLProtocol(JDBCUrl url, final String username, final String password, - Properties info) - throws QueryException, SQLException { + Properties info) { String fractionalSeconds = info.getProperty("useFractionalSeconds", "true"); if ("true".equalsIgnoreCase(fractionalSeconds)) { info.setProperty("useFractionalSeconds", "true"); @@ -276,24 +236,8 @@ public MySQLProtocol(JDBCUrl url, log.setLevel(Level.OFF); setDatatypeMappingFlags(); - parseHAOptions(); - connect(); } - private void parseHAOptions() { - String s = info.getProperty("autoReconnect"); - if (s != null && s.equals("true")) - autoReconnect = true; - s = info.getProperty("maxReconnects"); - if (s != null) - maxReconnects = Integer.parseInt(s); - s = info.getProperty("queriesBeforeRetryMaster"); - if (s != null) - queriesBeforeRetryMaster = Integer.parseInt(s); - s = info.getProperty("secondsBeforeRetryMaster"); - if (s != null) - secondsBeforeRetryMaster = Integer.parseInt(s); - } /** * Connect the client and perform handshake * @@ -514,18 +458,19 @@ void connect(String host, int port) throws QueryException, IOException, SQLExcep // At this point, the driver is connected to the database, if createDB is true, // then just try to create the database and to use it - if (createDB()) { - // Try to create the database if it does not exist - String quotedDB = MySQLConnection.quoteIdentifier(this.database); - executeQuery(new MySQLQuery("CREATE DATABASE IF NOT EXISTS " + quotedDB)); - executeQuery(new MySQLQuery("USE " + quotedDB)); + if (checkIfMaster()) { + if (createDB()) { + // Try to create the database if it does not exist + String quotedDB = MySQLConnection.quoteIdentifier(this.database); + executeQuery(new MySQLQuery("CREATE DATABASE IF NOT EXISTS " + quotedDB)); + executeQuery(new MySQLQuery("USE " + quotedDB)); + } } activeResult = null; moreResults = false; hasWarnings = false; connected = true; - hostFailed = false; // Prevent reconnects writer.setMaxAllowedPacket(this.maxAllowedPacket); } catch (IOException e) { throw new QueryException("Could not connect to " + host + ":" + @@ -536,7 +481,11 @@ void connect(String host, int port) throws QueryException, IOException, SQLExcep } } - + + public boolean checkIfMaster() throws SQLException { + return true; + } + private boolean isServerLanguageUTF8MB4(byte serverLanguage) { Byte[] utf8mb4Languages = { (byte)45,(byte)46,(byte)224,(byte)225,(byte)226,(byte)227,(byte)228, @@ -597,6 +546,7 @@ public PrepareResult(int statementId, MySQLColumnInformation[] columns, MySQLCo } } + @Override public PrepareResult prepare(String sql) throws QueryException { try { writer.startPacket(0); @@ -642,7 +592,8 @@ public PrepareResult prepare(String sql) throws QueryException { } } - public synchronized void closePreparedStatement(int statementId) throws QueryException{ + @Override + public synchronized void closePreparedStatement(int statementId) throws QueryException { try { writer.startPacket(0); writer.write(0x19); /*COM_STMT_CLOSE*/ @@ -654,95 +605,40 @@ public synchronized void closePreparedStatement(int statementId) throws QueryExc e); } } - public void setHostFailed() { - hostFailed = true; - failTimestamp = System.currentTimeMillis(); - } - - - public boolean shouldReconnect() { - return (!inTransaction() && hostFailed && autoReconnect && reconnectCount < maxReconnects); + public JDBCUrl getJdbcUrl() { + return jdbcUrl; } + @Override public boolean getAutocommit() { return ((serverStatus & ServerStatus.AUTOCOMMIT) != 0); } + public boolean isHighAvailability() { return false; } + public boolean isMasterConnection() { return true; } + @Override public boolean noBackslashEscapes() { return ((serverStatus & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); } - public void reconnectToMaster() throws IOException,QueryException, SQLException { - SyncPacketFetcher saveFetcher = this.packetFetcher; - PacketOutputStream saveWriter = this.writer; - Socket saveSocket = this.socket; - HostAddress[] addrs = jdbcUrl.getHostAddresses(); - boolean success = false; - try { - connect(addrs[0].host, addrs[0].port); - try { - close(saveFetcher, saveWriter, saveSocket); - } catch (Exception e) { - } - success = true; - } finally { - if (!success) { - failTimestamp = System.currentTimeMillis(); - queriesSinceFailover = 0; - this.packetFetcher = saveFetcher; - this.writer = saveWriter; - this.socket = saveSocket; - } - } - } + + @Override public void connect() throws QueryException, SQLException { if (!isClosed()) { close(); } - HostAddress[] addrs = jdbcUrl.getHostAddresses(); - - // There could be several addresses given in the URL spec, try all of them, and throw exception if all hosts - // fail. - for(int i = 0; i < addrs.length; i++) { - currentHost = addrs[i]; - try { - connect(currentHost.host, currentHost.port); - return; - } catch (IOException e) { - if (i == addrs.length - 1) { - throw new QueryException("Could not connect to " + HostAddress.toString(addrs) + - " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); - } - } + currentHost = this.jdbcUrl.getHostAddresses()[0]; + try { + connect(currentHost.host, currentHost.port); + return; + } catch (IOException e) { + throw new QueryException("Could not connect to " + HostAddress.toString(this.jdbcUrl.getHostAddresses()) + + " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); } } - public boolean isMasterConnection() { - return currentHost == jdbcUrl.getHostAddresses()[0]; - } - - /** - * Check if fail back to master connection is desired, - * @return - */ - public boolean shouldTryFailback() { - if (isMasterConnection()) - return false; - - if (inTransaction()) - return false; - if (reconnectCount >= maxReconnects) - return false; - - long now = System.currentTimeMillis(); - if ((now - failTimestamp)/1000 > secondsBeforeRetryMaster) - return true; - if (queriesSinceFailover > queriesBeforeRetryMaster) - return true; - return false; - } - public boolean inTransaction() - { + @Override + public boolean inTransaction() { return ((serverStatus & ServerStatus.IN_TRANSACTION) != 0); } @@ -759,6 +655,7 @@ private void setDatatypeMappingFlags() { } } + @Override public Properties getInfo() { return info; } @@ -773,14 +670,13 @@ void skip() throws IOException, QueryException{ } + @Override public boolean hasMoreResults() { return moreResults; } - private static void close(PacketFetcher fetcher, PacketOutputStream packetOutputStream, Socket socket) - throws QueryException - { + protected static void close(PacketFetcher fetcher, PacketOutputStream packetOutputStream, Socket socket) throws QueryException { ClosePacket closePacket = new ClosePacket(); try { try { @@ -810,6 +706,7 @@ private static void close(PacketFetcher fetcher, PacketOutputStream packetOutput * Closes socket and stream readers/writers * Attempts graceful shutdown. */ + @Override public void close() { try { /* If a streaming result set is open, close it.*/ @@ -818,13 +715,11 @@ public void close() { /* eat exception */ } try { - close(packetFetcher,writer, socket); - } - catch (Exception e) { + close(packetFetcher, writer, socket); + } catch (Exception e) { // socket is closed, so it is ok to ignore exception log.info("got exception " + e + " while closing connection"); - } - finally { + } finally { this.connected = false; } } @@ -832,6 +727,7 @@ public void close() { /** * @return true if the connection is closed */ + @Override public boolean isClosed() { return !this.connected; } @@ -852,6 +748,7 @@ private SelectQueryResult createQueryResult(final ResultSetPacket packet, boolea return CachedSelectResult.createCachedSelectResult(streamingResult); } + @Override public void selectDB(final String database) throws QueryException { log.finest("Selecting db " + database); final SelectDBPacket packet = new SelectDBPacket(database); @@ -868,39 +765,56 @@ public void selectDB(final String database) throws QueryException { this.database = database; } + @Override public String getServerVersion() { return version; } + public void initializeConnection() { + + }; + + @Override public void setReadonly(final boolean readOnly) { this.readOnly = readOnly; } + @Override public boolean getReadonly() { return readOnly; } + @Override + public HostAddress getHostAddress() { + return currentHost; + } + @Override public String getHost() { return currentHost.host; } + @Override public int getPort() { return currentHost.port; } + @Override public String getDatabase() { return database; } + @Override public String getUsername() { return username; } + @Override public String getPassword() { return password; } + @Override public boolean ping() throws QueryException { final MySQLPingPacket pingPacket = new MySQLPingPacket(); try { @@ -916,11 +830,13 @@ public boolean ping() throws QueryException { } } + @Override public QueryResult executeQuery(Query dQuery) throws QueryException, SQLException { return executeQuery(dQuery, false); } + @Override public QueryResult getResult(Query dQuery, boolean streaming) throws QueryException{ RawPacket rawPacket; ResultPacket resultPacket; @@ -1011,6 +927,7 @@ public QueryResult getResult(Query dQuery, boolean streaming) throws QueryExcept } } + @Override public QueryResult executeQuery(final Query dQuery, boolean streaming) throws QueryException, SQLException { dQuery.validate(); @@ -1036,8 +953,7 @@ public QueryResult executeQuery(final Query dQuery, boolean streaming) throws Qu SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); } - if (!isMasterConnection()) - queriesSinceFailover++; + try { return getResult(dQuery, streaming); } catch (QueryException qex) { @@ -1052,6 +968,7 @@ public QueryResult executeQuery(final Query dQuery, boolean streaming) throws Qu + @Override public String getServerVariable(String variable) throws QueryException, SQLException { CachedSelectResult qr = (CachedSelectResult) executeQuery(new MySQLQuery("select @@" + variable)); try { @@ -1081,12 +998,15 @@ public String getServerVariable(String variable) throws QueryException, SQLExcep * @throws QueryException * @throws SQLException */ + @Override public void cancelCurrentQuery() throws QueryException, IOException, SQLException { MySQLProtocol copiedProtocol = new MySQLProtocol(jdbcUrl, username, password, info); + copiedProtocol.connect(); copiedProtocol.executeQuery(new MySQLQuery("KILL QUERY " + serverThreadId)); copiedProtocol.close(); } + @Override public boolean createDB() { String alias = info.getProperty("createDatabaseIfNotExist"); return info != null @@ -1096,6 +1016,7 @@ public boolean createDB() { + @Override public QueryResult getMoreResults(boolean streaming) throws QueryException { if(!moreResults) return null; @@ -1122,10 +1043,12 @@ public static String hexdump(ByteBuffer bb, int offset) { } + @Override public boolean hasUnreadData() { return (activeResult != null); } + @Override public void setMaxRows(int max) throws QueryException, SQLException{ if (maxRows != max) { if (max == 0) { @@ -1136,6 +1059,9 @@ public void setMaxRows(int max) throws QueryException, SQLException{ maxRows = max; } } + public int getMaxRows() { + return maxRows; + } void parseVersion() { String[] a = version.split("[^0-9]"); @@ -1147,14 +1073,17 @@ void parseVersion() { patchVersion = Integer.parseInt(a[2]); } + @Override public int getMajorServerVersion() { return majorVersion; } + @Override public int getMinorServerVersion() { return minorVersion; } + @Override public boolean versionGreaterOrEqual(int major, int minor, int patch) { if (this.majorVersion > major) return true; @@ -1179,14 +1108,17 @@ public boolean versionGreaterOrEqual(int major, int minor, int patch) { /* Patch versions are equal => versions are equal */ return true; } - public void setLocalInfileInputStream(InputStream inputStream) { + @Override + public void setLocalInfileInputStream(InputStream inputStream) { this.localInfileInputStream = inputStream; } - public int getMaxAllowedPacket() { + @Override + public int getMaxAllowedPacket() { return this.maxAllowedPacket; } - public void setMaxAllowedPacket(int maxAllowedPacket) { + @Override + public void setMaxAllowedPacket(int maxAllowedPacket) { this.maxAllowedPacket = maxAllowedPacket; } @@ -1195,7 +1127,8 @@ public void setMaxAllowedPacket(int maxAllowedPacket) { * @param timeout the timeout, in milliseconds * @throws SocketException */ - public void setTimeout(int timeout) throws SocketException { + @Override + public void setTimeout(int timeout) throws SocketException { this.socket.setSoTimeout(timeout); } /** @@ -1203,12 +1136,27 @@ public void setTimeout(int timeout) throws SocketException { * @return * @throws SocketException */ - public int getTimeout() throws SocketException { + @Override + public int getTimeout() throws SocketException { return this.socket.getSoTimeout(); } - public String getPinGlobalTxToPhysicalConnection() { + @Override + public String getPinGlobalTxToPhysicalConnection() { return this.info.getProperty("pinGlobalTxToPhysicalConnection", "false"); } - + + + public boolean hasWarnings() { + return hasWarnings; + } + + + public int getDatatypeMappingFlags() { + return datatypeMappingFlags; + } + + public StreamingSelectResult getActiveResult() { + return activeResult; + } } diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/Protocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/Protocol.java new file mode 100644 index 000000000..1d9642723 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/Protocol.java @@ -0,0 +1,103 @@ +package org.mariadb.jdbc.internal.mysql; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.query.Query; +import org.mariadb.jdbc.internal.common.queryresults.QueryResult; +import org.mariadb.jdbc.internal.common.queryresults.StreamingSelectResult; + +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketException; +import java.sql.SQLException; +import java.util.Properties; + +/** + * Created by diego_000 on 12/05/2015. + */ +public interface Protocol { + MySQLProtocol.PrepareResult prepare(String sql) throws QueryException; + + void closePreparedStatement(int statementId) throws QueryException; + + boolean getAutocommit(); + + boolean noBackslashEscapes(); + + void connect() throws QueryException, SQLException; + void initializeConnection() throws QueryException, SQLException; + JDBCUrl getJdbcUrl(); + boolean inTransaction(); + + Properties getInfo(); + + boolean hasMoreResults(); + + void close(); + + boolean isClosed(); + + void selectDB(String database) throws QueryException; + + String getServerVersion(); + + void setReadonly(boolean readOnly); + + boolean getReadonly(); + boolean isMasterConnection(); + boolean isHighAvailability(); + HostAddress getHostAddress(); + String getHost(); + + int getPort(); + + String getDatabase(); + + String getUsername(); + + String getPassword(); + + boolean ping() throws QueryException; + + QueryResult executeQuery(Query dQuery) throws QueryException, SQLException; + + QueryResult getResult(Query dQuery, boolean streaming) throws QueryException; + + QueryResult executeQuery(Query dQuery, boolean streaming) throws QueryException, SQLException; + + String getServerVariable(String variable) throws QueryException, SQLException; + + void cancelCurrentQuery() throws QueryException, IOException, SQLException; + + boolean createDB(); + + QueryResult getMoreResults(boolean streaming) throws QueryException; + + boolean hasUnreadData(); + boolean checkIfMaster() throws SQLException ; + boolean hasWarnings(); + int getDatatypeMappingFlags(); + StreamingSelectResult getActiveResult(); + + void setMaxRows(int max) throws QueryException, SQLException; + int getMaxRows(); + + int getMajorServerVersion(); + + int getMinorServerVersion(); + + boolean versionGreaterOrEqual(int major, int minor, int patch); + + void setLocalInfileInputStream(InputStream inputStream); + + int getMaxAllowedPacket(); + + void setMaxAllowedPacket(int maxAllowedPacket); + + void setTimeout(int timeout) throws SocketException; + + int getTimeout() throws SocketException; + + String getPinGlobalTxToPhysicalConnection(); +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/SingleHostListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/SingleHostListener.java new file mode 100644 index 000000000..c2fa7a65f --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/SingleHostListener.java @@ -0,0 +1,144 @@ +/* +MariaDB Client for Java + +Copyright (c) 2012 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.mysql; + +import org.mariadb.jdbc.internal.SQLExceptionMapper; +import org.mariadb.jdbc.internal.common.QueryException; +import java.lang.reflect.Method; +import java.sql.SQLException; + + +public class SingleHostListener implements FailoverListener { + + private FailoverProxy proxy; + + public SingleHostListener() { } + + public void setProxy(FailoverProxy proxy) { + this.proxy = proxy; + } + + public void initializeConnection() throws QueryException, SQLException { + this.proxy.currentProtocol.connect(); + } + + public void preExecute() throws SQLException { + if (shouldReconnect()) { + try { + reconnectSingleHost(); + } catch (QueryException qe) { + SQLExceptionMapper.throwException(qe, null, null); + } + } + } + + + private boolean shouldReconnect() { + return (!proxy.currentProtocol.inTransaction() && proxy.masterHostFailTimestamp != 0 && proxy.autoReconnect && proxy.currentConnectionAttempts < proxy.maxReconnects); + } + + protected void reconnectSingleHost() throws QueryException, SQLException { + proxy.currentConnectionAttempts++; + proxy.currentProtocol.connect(); + + //if no error, reset failover variables + proxy.resetMasterFailoverData(); + } + + + public HandleErrorResult handleFailover(Method method, Object[] args) throws Throwable { + HandleErrorResult handleErrorResult = new HandleErrorResult(); + if (shouldReconnect()) { + //if not first attempt to connect, wait for initialTimeout + if (proxy.currentConnectionAttempts > 0) { + try { + Thread.sleep((long) proxy.initialTimeout * 1000); + } catch (InterruptedException IE) { + // ignore + } + } + + //trying to reconnect transparently + reconnectSingleHost(); + handleErrorResult.resultObject = method.invoke(proxy.currentProtocol, args); + handleErrorResult.mustThrowError = false; + return handleErrorResult; + } + return handleErrorResult; + } + + public void switchReadOnlyConnection(Boolean readonly) {} + public void postClose() throws SQLException { } + + public synchronized HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable { + HandleErrorResult handleErrorResult = new HandleErrorResult(); + if (shouldReconnect()) { + + //if not first attempt to connect, wait for initialTimeout + if (proxy.currentConnectionAttempts > 0) { + try { + Thread.sleep(proxy.initialTimeout * 1000); + } catch (InterruptedException e) { } + } + + //trying to reconnect transparently + reconnectSingleHost(); + handleErrorResult.resultObject = method.invoke(proxy.currentProtocol, args); + handleErrorResult.mustThrowError = false; + return handleErrorResult; + } + return handleErrorResult; + } + + public synchronized HandleErrorResult secondaryFail(Method method, Object[] args) throws Throwable { + return new HandleErrorResult(); + } + +} diff --git a/src/test/java/org/mariadb/jdbc/AuroraFailoverTest.java b/src/test/java/org/mariadb/jdbc/AuroraFailoverTest.java new file mode 100644 index 000000000..8731c6f29 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/AuroraFailoverTest.java @@ -0,0 +1,93 @@ +package org.mariadb.jdbc; + +import junit.framework.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.Assert.fail; + +/** + * Created by diego_000 on 17/05/2015. + */ +public class AuroraFailoverTest { + + static { Logger.getLogger("").setLevel(Level.FINEST); } + private final static Logger log = Logger.getLogger(FailoverTest.class.getName()); + + //the active connection + protected Connection connection; + //default multi-host URL + protected static final String defaultUrl = "jdbc:mysql://host1,host2,host3:3306/test?user=root"; + //hosts + protected String[] hosts; + + @Before + public void beforeClassFailover() throws SQLException { + //get the multi-host connection string + String url = System.getProperty("dbUrl", defaultUrl); + //parse the url + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + connection = DriverManager.getConnection(url); + } + + @Test + public void simulateChangeToReadonlyHost() throws SQLException{ + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("Connect to master : READ ONLY : " + rs.getString(2)); + Assert.assertTrue("OFF".equals(rs.getString(2))); + + //switching to read-onlyConnection + connection.setReadOnly(true); + + //verification that secondary take place + rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("verifying switch to replica : READ ONLY : " + rs.getString(2)); + Assert.assertFalse("OFF".equals(rs.getString(2))); + + //simulate master crash + try { + stmt.execute("ALTER SYSTEM CRASH"); + } catch ( Exception e) { } + + rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("verifying switch temporary to master : " + rs.getString(2)); + Assert.assertTrue("OFF".equals(rs.getString(2))); + + try { + Thread.sleep((long) 90 * 1000); + } catch (InterruptedException IE) { } + + //after 90 second, the ALTER SYSTEM CRASH has ended, so the master must have been relaunched. + //switching to master connection + connection.setReadOnly(false); + + //verification that secondary take place + rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("verifying switch to replica : READ ONLY : " + rs.getString(2)); + Assert.assertTrue("OFF".equals(rs.getString(2))); + } + + @After + public void after() throws SQLException { + try { + connection.close(); + } catch(Exception e) { + logInfo(e.toString()); + } + } + + // common function for logging information + static void logInfo(String message) { + log.info(message); + } +} diff --git a/src/test/java/org/mariadb/jdbc/Failover.java b/src/test/java/org/mariadb/jdbc/Failover.java deleted file mode 100644 index 945acb3e0..000000000 --- a/src/test/java/org/mariadb/jdbc/Failover.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.mariadb.jdbc; - -import static org.junit.Assert.*; - -import java.sql.Connection; -import java.sql.SQLException; - -import javax.sql.DataSource; - -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -public class Failover { - //the active connection - protected Connection connection; - //default multi-host URL - protected static final String defaultUrl = "jdbc:mysql://host1,host2,host3:3306/test?user=root"; - //hosts - protected String[] hosts; - - @BeforeClass - public static void beforeClassFailover() { - //get the multi-host connection string - String url = System.getProperty("dbUrl", defaultUrl); - //parse the url - //TODO JDBCUrl cannot parse the multi-hosts - JDBCUrl jdbcUrl = JDBCUrl.parse(url); - //TODO store hosts in list - - //TODO add support for the following url properties - //autoReconnect, maxReconnects, queriesBeforeRetryMaster, secondsBeforeRetryMaster - } - - @Before - public void before() throws SQLException{ - //get the new multi-host connection - //TODO use DataSource - //TODO MySQLConnection need to support multi-host - } - - @Test - public void simulateConnectingToFirstHost() - { - fail("Not implemented"); - } - - @Test - public void simulateFailingFirstHost() - { - fail("Not implemented"); - } - - @Test - public void simulateTwoFirstHostsDown() - { - fail("Not implemented"); - } - - @Test - public void simulateAllHostsDown() - { - fail("Not implemented"); - } - - @Test - public void loadBalance() - { - fail("Not implemented"); - } - - @After - public void after() throws SQLException { - try - { - connection.close(); - } - catch(Exception e) - { - logInfo(e.toString()); - } - } - - // common function for logging information - static void logInfo(String message) - { - System.out.println(message); - } -} diff --git a/src/test/java/org/mariadb/jdbc/FailoverTest.java b/src/test/java/org/mariadb/jdbc/FailoverTest.java new file mode 100644 index 000000000..55939eb74 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/FailoverTest.java @@ -0,0 +1,100 @@ +package org.mariadb.jdbc; + +import static org.junit.Assert.*; + +import java.sql.*; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.sql.DataSource; + +import junit.framework.Assert; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class FailoverTest { + static { Logger.getLogger("").setLevel(Level.FINEST); } + private final static Logger log = Logger.getLogger(FailoverTest.class.getName()); + + //the active connection + protected Connection connection; + //default multi-host URL + protected static final String defaultUrl = "jdbc:mysql://host1,host2,host3:3306/test?user=root"; + //hosts + protected String[] hosts; + + @Before + public void beforeClassFailover() throws SQLException { + //get the multi-host connection string + String url = System.getProperty("dbUrl", defaultUrl); + //parse the url + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + connection = DriverManager.getConnection(url); + } + + + @Test + public void simulateConnectingToFirstHost() throws SQLException { + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + Assert.assertTrue("OFF".equals(rs.getString(2))); + } + + @Test + public void simulateFailingFirstHost() throws SQLException{ + Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("READ ONLY : " + rs.getString(2)); + Assert.assertTrue("OFF".equals(rs.getString(2))); + + //simulate master crash + try { + stmt.execute("ALTER SYSTEM CRASH"); + } catch ( Exception e) { } + + //verification that secondary take place + rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); + rs.next(); + log.info("READ ONLY : " + rs.getString(2)); + Assert.assertTrue("ON".equals(rs.getString(2))); + } + + @Test + public void simulateTwoFirstHostsDown() { + fail("Not implemented"); + } + + @Test + public void checkSessionInfoWithConnectionChange() { + fail("Not implemented"); + } + + @Test + public void simulateAllHostsDown() { + fail("Not implemented"); + } + + + @Test + public void loadBalance() { + fail("Not implemented"); + } + + @After + public void after() throws SQLException { + try { + connection.close(); + } catch(Exception e) { + logInfo(e.toString()); + } + } + + // common function for logging information + static void logInfo(String message) { + log.info(message); + } +}