diff --git a/README.md b/README.md index 51d438816..021479781 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ MariaDB Connector/J is used to connect applications developed in Java to MariaDB and MySQL databases. MariaDB Connector/J is LGPL licensed. Tracker link https://mariadb.atlassian.net/projects/CONJ/issues/ - + ## Obtaining the driver The driver (jar) can be downloaded from [mariadb connector download](https://mariadb.com/products/connectors-plugins) diff --git a/build.xml b/build.xml new file mode 100644 index 000000000..7ba58a651 --- /dev/null +++ b/build.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/maven-build.properties b/maven-build.properties new file mode 100644 index 000000000..d5d55b600 --- /dev/null +++ b/maven-build.properties @@ -0,0 +1,22 @@ +#Generated by Maven Ant Plugin - DO NOT EDIT THIS FILE! +#Mon Jul 13 22:05:03 CEST 2015 +maven.settings.offline=false +maven.build.finalName=mariadb-java-client-1.2.0-SNAPSHOT +maven.build.resourceDir.0=src/main/resources +maven.build.testOutputDir=${maven.build.dir}/test-classes +maven.build.testResourceDir.0=src/test/resources +jna.version=3.3.0 +maven.reporting.outputDirectory=${maven.build.dir}/site +project.build.sourceEncoding=UTF-8 +maven.build.srcDir.0=src/main/java +sonatypeOssDistMgmtSnapshotsUrl=https\://oss.sonatype.org/content/repositories/snapshots/ +project.build.directory=${maven.build.dir} +maven.test.reports=${maven.build.dir}/test-reports +maven.build.dir=target +project.build.outputDirectory=${maven.build.outputDir} +version.file=src/main/java/org/mariadb/jdbc/Version.java +version.template.file=src/main/resources/Version.java.template +maven.build.testDir.0=src/test/java +maven.settings.interactiveMode=true +maven.repo.local=${user.home}/.m2/repository +maven.build.outputDir=${maven.build.dir}/classes diff --git a/maven-build.xml b/maven-build.xml new file mode 100644 index 000000000..2beb82ede --- /dev/null +++ b/maven-build.xml @@ -0,0 +1,305 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + =================================== WARNING =================================== + JUnit is not present in the test classpath or your $ANT_HOME/lib directory. Tests not executed. + =============================================================================== + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 0495b288a..ac6430ef9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,9 +5,9 @@ mariadb-java-client jar mariadb-java-client - 1.1.10-SNAPSHOT + 1.2.0-SNAPSHOT JDBC driver for MariaDB and MySQL - https://kb.askmonty.org/en/about-the-mariadb-java-client/ + https://mariadb.com/kb/en/mariadb/about-mariadb-connector-j/ UTF-8 3.3.0 @@ -44,8 +44,38 @@ Georg Richter georg@skysql.com + + diego + Diego Dupin + diego.dupin@mariadb.com + + + test + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + -Xmx1g + + + java.util.logging.config.file + src/test/resources/logging.properties + + + + + + + + package-source @@ -58,7 +88,7 @@ org.codehaus.mojo exec-maven-plugin - 1.2 + 1.2.1 git @@ -81,19 +111,25 @@ org.apache.maven.plugins maven-compiler-plugin - 3.2 + 3.3 + - 1.6 - 1.6 + + 1.7 + 1.7 org.apache.maven.plugins maven-jar-plugin - 2.5 + 2.6 + + **/logging.properties + - src/main/resources/META-INF/MANIFEST.MF ${project.version}.0 @@ -108,37 +144,6 @@ - - org.apache.maven.plugins - maven-surefire-plugin - 2.12.4 - - -Xmx1g - - - - - com.atlassian.maven.plugins - maven-clover2-plugin - 4.0.2 - - /etc/clover.license - - org.apache.maven.plugins maven-source-plugin @@ -154,7 +159,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 2.10.1 + 2.10.3 attach-javadocs @@ -164,7 +169,19 @@ - + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + parse-version + + parse-version + + + + com.google.code.maven-replacer-plugin maven-replacer-plugin @@ -182,13 +199,30 @@ ${version.file} - @buildtime@ + @buildtime ${maven.build.timestamp} - @pomversion@ + @version ${project.version} + + @majorVersion + ${parsedVersion.majorVersion} + + + @minorVersion + ${parsedVersion.minorVersion} + + + @patchVersion + ${parsedVersion.incrementalVersion} + + + @qualifier + ${parsedVersion.qualifier} + + parsedVersion.qualifier @@ -199,7 +233,7 @@ junit junit - 4.6 + 4.12 test @@ -221,53 +255,14 @@ - - org.apache.maven.plugins - maven-clover-plugin - 2.4 - - /etc/clover.license - - - - org.apache.maven.plugins - maven-checkstyle-plugin - 2.4 - - mariadb-jdbc-checks.xml - - org.apache.maven.plugins maven-javadoc-plugin - 2.1.0 - - - org.apache.maven.plugins - maven-pmd-plugin - 3.2 - - utf-8 - 100 - 1.6 - - - - org.apache.maven.plugins - maven-surefire-report-plugin - 2.18 + 2.10.3 - - org.codehaus.mojo - findbugs-maven-plugin - 2.1 - - true - - - + org.sonatype.oss oss-parent diff --git a/src/main/java/org/mariadb/jdbc/Driver.java b/src/main/java/org/mariadb/jdbc/Driver.java index 230c090a0..0ddde8cb1 100644 --- a/src/main/java/org/mariadb/jdbc/Driver.java +++ b/src/main/java/org/mariadb/jdbc/Driver.java @@ -52,14 +52,16 @@ 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; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Logger; @@ -79,48 +81,33 @@ public final class Driver implements java.sql.Driver { /** * Connect to the given connection string. - * + * * the properties are currently ignored * * @param url the url to connect to - * @param info the properties of the connection - ignored at the moment * @return a connection * @throws SQLException if it is not possible to connect */ - public Connection connect(final String url, final Properties info) throws SQLException { - // TODO: handle the properties! - // TODO: define what props we support! - - String baseUrl = url; - int idx = url.lastIndexOf("?"); - if(idx > 0) { - baseUrl = url.substring(0,idx); - String urlParams = url.substring(idx+1); - setURLParameters(urlParams, info); - } + public Connection connect(final String url, final Properties props) throws SQLException { log.finest("Connecting to: " + url); try { - final JDBCUrl jdbcUrl = JDBCUrl.parse(baseUrl); - if(jdbcUrl == null) { + JDBCUrl jdbcUrl = JDBCUrl.parse(url, props); + if (jdbcUrl.getHostAddresses() == null) { + log.info("MariaDB connector : missing Host address"); return null; + } else { + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + Protocol protocol = Utils.retrieveProxy(jdbcUrl, lock); + return MySQLConnection.newConnection(protocol, lock); } - String userName = info.getProperty("user",jdbcUrl.getUsername()); - String password = info.getProperty("password",jdbcUrl.getPassword()); - - MySQLProtocol protocol = new MySQLProtocol(jdbcUrl, userName, password, info); - return MySQLConnection.newConnection(protocol); } catch (QueryException e) { SQLExceptionMapper.throwException(e, null, null); return null; } } - private void setURLParameters(String urlParameters, Properties info) { - Utils.setUrlParameters(urlParameters, info); - } - /** * returns true if the driver can accept the url. * @@ -151,7 +138,7 @@ public DriverPropertyInfo[] getPropertyInfo(String url, * @return the major versions */ public int getMajorVersion() { - return 1; + return Version.majorVersion; } /** @@ -160,7 +147,7 @@ public int getMajorVersion() { * @return the minor version */ public int getMinorVersion() { - return 1; + return Version.minorVersion; } /** @@ -172,10 +159,10 @@ public boolean jdbcCompliant() { return false; } - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - // TODO Auto-generated method stub - return null; - } + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + // TODO Auto-generated method stub + return null; + } /* Provide a "cleanup" method that can be called after unloading driver, to fix Tomcat's obscure classpath handling. diff --git a/src/main/java/org/mariadb/jdbc/HostAddress.java b/src/main/java/org/mariadb/jdbc/HostAddress.java index 8c21e9e15..a5b3b1b7e 100644 --- a/src/main/java/org/mariadb/jdbc/HostAddress.java +++ b/src/main/java/org/mariadb/jdbc/HostAddress.java @@ -1,68 +1,164 @@ package org.mariadb.jdbc; +import org.mariadb.jdbc.internal.common.ParameterConstant; +import org.mariadb.jdbc.internal.common.UrlHAMode; + +import java.util.ArrayList; +import java.util.List; + public class HostAddress { public String host; public int port; + public String type=null; + /** * parse - parse server addresses from the URL fragment * @param spec - list of endpoints in one of the forms * 1 - host1,....,hostN:port (missing port default to MySQL default 3306 * 2 - host:port,...,host:port + * @param haMode High availability mode * @return parsed endpoints */ - public static HostAddress[] parse(String spec) { - String[] tokens = spec.split(","); - HostAddress[] arr = new HostAddress[tokens.length]; + public static List parse(String spec, UrlHAMode haMode) { + if (spec == null) throw new IllegalArgumentException("Invalid connection URL, host address must not be empty "); + String[] tokens = spec.trim().split(","); + List arr = new ArrayList<>(tokens.length); for (int i=0; i < tokens.length; i++) { - arr[i] = parseSingleHostAddress(tokens[i]); + if (tokens[i].startsWith("address=")) { + arr.add(parseParameterHostAddress(tokens[i])); + } else { + arr.add(parseSimpleHostAddress(tokens[i])); + } } - int defaultPort = arr[arr.length-1].port; + int defaultPort = arr.get(arr.size()-1).port; if (defaultPort == 0) { defaultPort = 3306; } - for (int i = 0; i < arr.length; i++) { - if (arr[i].port == 0) { - arr[i].port = defaultPort; + for (int i = 0; i < arr.size(); i++) { + if (haMode == UrlHAMode.REPLICATION) { + if (i==0 && arr.get(i).type == null) arr.get(i).type = ParameterConstant.TYPE_MASTER; + else if (i!=0 && arr.get(i).type == null) arr.get(i).type = ParameterConstant.TYPE_SLAVE; + } + if (arr.get(i).port == 0) { + arr.get(i).port = defaultPort; } } return arr; } + public HostAddress() {} + + public HostAddress(String host, int port) { + this.host = host; + this.port = port; + this.type = ParameterConstant.TYPE_MASTER; + } + + public HostAddress(String host, int port, String type) { + this.host = host; + this.port = port; + this.type = type; + } - static HostAddress parseSingleHostAddress(String s) { - HostAddress result = new HostAddress(); - if (s.startsWith("[")) { + static HostAddress parseSimpleHostAddress(String s) { + HostAddress result = new HostAddress(); + if (s.startsWith("[")) { /* IPv6 addresses in URLs are enclosed in square brackets */ - int ind = s.indexOf(']'); - result.host = s.substring(1,ind); - if (ind != (s.length() -1) && s.charAt(ind + 1) == ':') { - result.port = Integer.parseInt(s.substring(ind+2)); - } - } - else if (s.contains(":")) { + int ind = s.indexOf(']'); + result.host = s.substring(1,ind); + if (ind != (s.length() -1) && s.charAt(ind + 1) == ':') { + result.port = Integer.parseInt(s.substring(ind+2)); + } + } else if (s.contains(":")) { /* Parse host:port */ - String[] hostPort = s.split(":"); - result.host = hostPort[0]; - result.port = Integer.parseInt(hostPort[1]); - } else { + String[] hostPort = s.split(":"); + result.host = hostPort[0]; + result.port = Integer.parseInt(hostPort[1]); + } else { /* Just host name is given */ - result.host = s; - } - return result; + result.host = s; + } + return result; + } + static HostAddress parseParameterHostAddress(String s) { + HostAddress result = new HostAddress(); + String[] array = s.split("(?=\\()|(?<=\\))"); + for (int i=1;i< array.length;i++) { + String[] token = array[i].replace("(","").replace(")","").trim().split("="); + if (token.length != 2) throw new IllegalArgumentException("Invalid connection URL, expected key=value pairs, found " + array[i]); + String key = token[0].toLowerCase(); + String value = token[1].toLowerCase(); + if (key.equals("host")) { + result.host=value.replace("[","").replace("]", ""); + } else if (key.equals("port")) { + result.port=Integer.parseInt(value); + } else if (key.equals("type")) { + if (value.equals(ParameterConstant.TYPE_MASTER) || value.equals(ParameterConstant.TYPE_SLAVE))result.type=value; + } + } + return result; } + + public static String toString(List addrs) { + String s=""; + for(int i=0; i < addrs.size(); i++) { + if (addrs.get(i).type != null) { + s+="address=(host="+addrs.get(i).host+")(port="+addrs.get(i).port+")(type="+addrs.get(i).type+")"; + } else { + 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++) { - boolean isIPv6 = addrs[i].host != null && addrs[i].host.contains(":"); - String host = (isIPv6)?("[" + addrs[i].host + "]"):addrs[i].host; - s += host + ":" + addrs[i].port; - if (i < addrs.length -1) - s += ","; + if (addrs[i].type != null) { + s+="address=(host="+addrs[i].host+")(port="+addrs[i].port+")(type="+addrs[i].type+")"; + } else { + boolean isIPv6 = addrs[i].host != null && addrs[i].host.contains(":"); + String host = (isIPv6)?("[" + addrs[i].host + "]"):addrs[i].host; + s += host + ":" + addrs[i].port; + } + if (i < addrs.length -1) s += ","; } return s; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HostAddress that = (HostAddress) o; + + if (port != that.port) return false; + if (host != null ? !host.equals(that.host) : that.host != null) return false; + return !(type != null ? !type.equals(that.type) : that.type != 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='" + host + '\'' + + ", port=" + port + + ", type='" + type + '\'' + + '}'; + } } diff --git a/src/main/java/org/mariadb/jdbc/JDBCUrl.java b/src/main/java/org/mariadb/jdbc/JDBCUrl.java index 8d2909db9..813e21da7 100644 --- a/src/main/java/org/mariadb/jdbc/JDBCUrl.java +++ b/src/main/java/org/mariadb/jdbc/JDBCUrl.java @@ -49,115 +49,208 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS package org.mariadb.jdbc; +import org.mariadb.jdbc.internal.common.DefaultOptions; +import org.mariadb.jdbc.internal.common.Options; +import org.mariadb.jdbc.internal.common.ParameterConstant; +import org.mariadb.jdbc.internal.common.UrlHAMode; +import org.mariadb.jdbc.internal.common.query.IllegalParameterException; + +import java.util.List; +import java.util.Properties; + +/** + *

parse and verification of URL.

+ * + * + *

basic syntax :
+ * {@code jdbc:(mysql|mariadb):[replication:|loadbalance:|aurora:]//[,]/[database>][?=[&=]] } + *

+ *

+ * hostDescription:
+ * - simple :
+ * {@code :}
+ * (for example localhost:3306)

+ * - complex :
+ * {@code address=[(type=(master|slave))][(port=)](host=)}
+ *

+ * type is by default master
+ * port is by default 3306
+ *

+ *

+ * host can be dns name, ipv4 or ipv6.
+ * in case of ipv6 and simple host description, the ip must be written inside bracket.
+ * exemple : {@code jdbc:mysql://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306}
+ *

+ *

+ * Some examples :
+ * {@code jdbc:mysql://localhost:3306/database?user=greg&password=pass}
+ * {@code jdbc:mysql://address=(type=master)(host=master1),address=(port=3307)(type=slave)(host=slave1)/database?user=greg&password=pass}
+ *

+ */ public class JDBCUrl { - private String username; - private String password; + private String database; - private HostAddress addresses[]; + private Options options; + private List addresses; + private UrlHAMode haMode; + private JDBCUrl(){}; - private JDBCUrl( String username, String password, String database, HostAddress addresses[]) { - this.username = username; - this.password = password; + protected JDBCUrl(String database, List addresses, Options options, UrlHAMode haMode) { + this.options = options; this.database = database; this.addresses = addresses; - } - - /* - Parse ConnectorJ compatible urls - jdbc:mysql://host:port/database - Example: jdbc:mysql://localhost:3306/test?user=root&password=passwd - */ - private static JDBCUrl parseConnectorJUrl(String url) { - if (!url.startsWith("jdbc:mysql://")) { - return null; + this.haMode = haMode; + if (haMode == UrlHAMode.AURORA) { + for (HostAddress hostAddress : addresses) hostAddress.type = null; + } else { + for (HostAddress hostAddress : addresses) { + if (hostAddress.type == null)hostAddress.type = ParameterConstant.TYPE_MASTER; + } } - - url = url.substring(13); - - String hostname; - String database; - String user = ""; - String password = ""; - String[] tokens = url.split("/"); - - hostname = tokens[0]; - database = (tokens.length > 1) ? tokens[1] : null; - - if (database == null) { - return new JDBCUrl("", "", database, HostAddress.parse(hostname)); - } - - //check if there are parameters - 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++) - { - if (credentials[i].startsWith("user=")) - user=credentials[i].substring(5); - else if (credentials[i].startsWith("password=")) - password = credentials[i].substring(9); - } - } - - return new JDBCUrl(user, password, database, HostAddress.parse(hostname)); } + + + + static boolean acceptsURL(String url) { - return (url != null) && - (url.startsWith("jdbc:mariadb://") || url.startsWith("jdbc:mysql://")) && - !(url.startsWith("jdbc:mysql://address=")); - + return (url != null) && + (url.startsWith("jdbc:mariadb://") || url.startsWith("jdbc:mysql://")); + } - public static JDBCUrl parse(final String url) { - if(url.startsWith("jdbc:mysql://")) { - return parseConnectorJUrl(url); + return parse(url, new Properties()); + } + + public static JDBCUrl parse(final String url, Properties prop) { + if (url != null) { + if (prop == null) prop = new Properties(); + if (url.startsWith("jdbc:mysql:")) { + return parseNewObject(url, prop); + } + String[] arr = new String[]{"jdbc:mysql:thin:", "jdbc:mariadb:"}; + for (String prefix : arr) { + if (url.startsWith(prefix)) { + return parseNewObject("jdbc:mysql:" + url.substring(prefix.length()), prop); + } + } + } + throw new IllegalArgumentException("Invalid connection URL url " + url); + } + + private static JDBCUrl parseNewObject(String url, Properties properties) { + if (!url.startsWith("jdbc:mysql:")) return null; + JDBCUrl jdbcUrl = new JDBCUrl(); + parseInternal(jdbcUrl, url, properties); + return jdbcUrl; + } + + public void parseUrl(String url) { + if (!url.startsWith("jdbc:mysql:")) throw new IllegalArgumentException("Url must start with \"jdbc:mysql:\""); + parseInternal(this, url, new Properties()); + } + + /* + Parse ConnectorJ compatible urls + jdbc:mysql://host:port/database + Example: jdbc:mysql://localhost:3306/test?user=root&password=passwd + */ + private static void parseInternal(JDBCUrl jdbcUrl, String url, Properties properties) { + + String[] baseTokens = url.substring(0,url.indexOf("//")).split(":"); + + //parse HA mode + jdbcUrl.haMode = UrlHAMode.NONE; + if (baseTokens.length > 2) { + try { + jdbcUrl.haMode = UrlHAMode.valueOf(baseTokens[2].toUpperCase()); + }catch (IllegalArgumentException i) { + throw new IllegalArgumentException("url parameter error '" + baseTokens[2] +"' is a unknown parameter in the url "+url); + } } - String[] arr = new String[] {"jdbc:mysql:thin://","jdbc:mariadb://"}; - for (String prefix : arr) { - if (url.startsWith(prefix)) { - return parseConnectorJUrl("jdbc:mysql://" + url.substring(prefix.length())); - } + + url = url.substring(url.indexOf("//") + 2); + String[] tokens = url.split("/"); + String hostAddressesString= tokens[0]; + String additionalParameters = (tokens.length > 1) ? url.substring(tokens[0].length() + 1) : null; + + jdbcUrl.addresses = HostAddress.parse(hostAddressesString, jdbcUrl.haMode); + + if (additionalParameters == null) { + jdbcUrl.database = null; + jdbcUrl.options = DefaultOptions.parse(jdbcUrl.haMode, "",properties); + return; } - return null; + + int ind = additionalParameters.indexOf('?'); + if (ind > -1) { + jdbcUrl.database = additionalParameters.substring(0, ind); + jdbcUrl.options = DefaultOptions.parse(jdbcUrl.haMode, additionalParameters.substring(ind + 1),properties); + } else { + jdbcUrl.database = additionalParameters; + jdbcUrl.options = DefaultOptions.parse(jdbcUrl.haMode, "",properties); + } + + if (jdbcUrl.haMode == UrlHAMode.AURORA) { + for (HostAddress hostAddress : jdbcUrl.addresses) hostAddress.type = null; + } else { + for (HostAddress hostAddress : jdbcUrl.addresses) { + if (hostAddress.type == null) hostAddress.type = ParameterConstant.TYPE_MASTER; + } + } + } + public String getUsername() { - return username; + return options.user; } public String getPassword() { - return password; + return options.password; } - public String getHostname() { - return addresses[0].host; + public String getDatabase() { + return database; } - public int getPort() { - return addresses[0].port; + public List getHostAddresses() { + return this.addresses; } - public String getDatabase() { - return database; + protected void setUsername(String username) { + options.user = username; + } + + protected void setPassword(String password) { + options.password = password; + } + + public Options getOptions() { + return options; } + protected void setDatabase(String database) { + this.database = database; + } - public HostAddress[] getHostAddresses() { - return this.addresses; + protected void setProperties(String urlParameters) { + DefaultOptions.parse(this.haMode, urlParameters, this.options); } public String toString() { String s = "jdbc:mysql://"; + if (!haMode.equals(UrlHAMode.NONE)) s = "jdbc:mysql:"+haMode.toString().toLowerCase()+"://"; if (addresses != null) s += HostAddress.toString(addresses); if (database != null) s += "/" + database; - return s; + return s; } + + public UrlHAMode getHaMode() { + return haMode; + } + } diff --git a/src/main/java/org/mariadb/jdbc/MySQLCallableStatement.java b/src/main/java/org/mariadb/jdbc/MySQLCallableStatement.java index 9026bb496..dc26c82eb 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLCallableStatement.java +++ b/src/main/java/org/mariadb/jdbc/MySQLCallableStatement.java @@ -1239,23 +1239,26 @@ public void setObject(int parameterIndex, Object x) throws SQLException { } public boolean execute() throws SQLException { - synchronized (con.getProtocol()) { - if (rsOutputParameters != null) { - rsOutputParameters.close(); - rsOutputParameters = null; - } - if(parametersCount > 0) { - preparedStatement.execute(); - } - boolean ret = callStatement.execute(callQuery); - - // Read off output parameters, if there are any - // (but not if query is streaming) - if (hasOutputParameters() && callStatement.getFetchSize() != Integer.MIN_VALUE) { - readOutputParameters(); - } - return ret; - } + con.lock.writeLock().lock(); + try { + if (rsOutputParameters != null) { + rsOutputParameters.close(); + rsOutputParameters = null; + } + if (parametersCount > 0) { + preparedStatement.execute(); + } + boolean ret = callStatement.execute(callQuery); + + // Read off output parameters, if there are any + // (but not if query is streaming) + if (hasOutputParameters() && callStatement.getFetchSize() != Integer.MIN_VALUE) { + readOutputParameters(); + } + return ret; + } finally { + con.lock.writeLock().unlock(); + } } public void addBatch() throws SQLException { diff --git a/src/main/java/org/mariadb/jdbc/MySQLConnection.java b/src/main/java/org/mariadb/jdbc/MySQLConnection.java index 1e2182141..d989ec664 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLConnection.java +++ b/src/main/java/org/mariadb/jdbc/MySQLConnection.java @@ -50,21 +50,26 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS package org.mariadb.jdbc; import org.mariadb.jdbc.internal.SQLExceptionMapper; +import org.mariadb.jdbc.internal.common.DefaultOptions; +import org.mariadb.jdbc.internal.common.Options; 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.*; import java.util.*; import java.util.concurrent.Executor; +import java.util.concurrent.locks.ReentrantReadWriteLock; public final class MySQLConnection implements Connection { /** * the protocol to communicate with. */ - private final MySQLProtocol protocol; + private final Protocol protocol; + public final ReentrantReadWriteLock lock; + /** * save point count - to generate good names for the savepoints. */ @@ -72,7 +77,7 @@ public final class MySQLConnection implements Connection { /** * the properties for the client. */ - private final Properties clientInfoProperties; + private Options options; public MySQLPooledConnection pooledConnection; @@ -88,13 +93,14 @@ public final class MySQLConnection implements Connection { * * @param protocol the protocol to use. */ - private MySQLConnection(MySQLProtocol protocol) { + private MySQLConnection(Protocol protocol, ReentrantReadWriteLock lock) throws SQLException { this.protocol = protocol; - clientInfoProperties = protocol.getInfo(); + options = protocol.getOptions(); + this.lock = lock; } - - MySQLProtocol getProtocol() { - return protocol; + + Protocol getProtocol() { + return protocol; } static TimeZone getTimeZone(String id) throws SQLException { @@ -106,23 +112,21 @@ static TimeZone getTimeZone(String id) throws SQLException { } return tz; } - - public static MySQLConnection newConnection(MySQLProtocol protocol) throws SQLException { - MySQLConnection connection = new MySQLConnection(protocol); - Properties info = protocol.getInfo(); - boolean fastConnect = info.get("fastConnect") != null ; - String sessionVariables = info.getProperty("sessionVariables"); - String timeZoneId = info.getProperty("serverTimezone"); - if (timeZoneId != null) { - TimeZone tz = getTimeZone(timeZoneId); + public static MySQLConnection newConnection(Protocol protocol, ReentrantReadWriteLock lock) throws SQLException { + MySQLConnection connection = new MySQLConnection(protocol, lock); + + Options options = protocol.getOptions(); + if (options.serverTimezone != null) { + TimeZone tz = getTimeZone(options.serverTimezone); connection.cal = Calendar.getInstance(tz); } - connection.noBackslashEscapes = protocol.noBackslashEscapes(); - String nullCatalogMeansCurrentString = info.getProperty("nullCatalogMeansCurrent"); - if (nullCatalogMeansCurrentString != null && nullCatalogMeansCurrentString.equals("false")) { - connection.nullCatalogMeansCurrent = false; + try { + connection.noBackslashEscapes = protocol.noBackslashEscapes(); + } catch (QueryException e) { + SQLExceptionMapper.throwException(e, connection, null); } + connection.nullCatalogMeansCurrent = options.nullCatalogMeansCurrent; return connection; } @@ -146,12 +150,25 @@ int getAutoIncrementIncrement() { * @throws SQLException if we cannot create the statement. */ public Statement createStatement() throws SQLException { - if (getProtocol().isClosed()) { - throw new SQLException("Cannot create a statement: closed connection"); - } + checkConnection(); return new MySQLStatement(this); } + private void checkConnection() throws SQLException { + if (protocol.isExplicitClosed()) { + throw new SQLException("createStatement() is called on closed connection"); + } + if (protocol.isClosed()) { + if (protocol.getProxy() != null) { + lock.writeLock().lock(); + try { + protocol.getProxy().reconnect(); + } finally { + lock.writeLock().unlock(); + } + } + } + } /** * creates a new prepared statement. Only client side prepared statement emulation right now. * @@ -160,12 +177,14 @@ public Statement createStatement() throws SQLException { * @throws SQLException if there is a problem preparing the statement. */ public PreparedStatement prepareStatement(final String sql) throws SQLException { + checkConnection(); return new MySQLPreparedStatement(this, sql); } public CallableStatement prepareCall(final String sql) throws SQLException { - return new MySQLCallableStatement(this, sql); + checkConnection(); + return new MySQLCallableStatement(this, sql); } @@ -182,7 +201,7 @@ public String nativeSQL(final String sql) throws SQLException { public void setAutoCommit(boolean autoCommit) throws SQLException { if (autoCommit == getAutoCommit()) return; - + Statement stmt = createStatement(); try { stmt.executeUpdate("set autocommit="+((autoCommit)?"1":"0")); @@ -193,7 +212,7 @@ public void setAutoCommit(boolean autoCommit) throws SQLException { /** * returns true if statements on this connection are auto commited. - * + * * @return true if auto commit is on. * @throws SQLException if there is an error */ @@ -243,7 +262,7 @@ public void close() throws SQLException { pooledConnection.fireConnectionClosed(); return; } - protocol.close(); + protocol.closeExplicit(); } /** @@ -264,7 +283,7 @@ public boolean isClosed() throws SQLException { */ public DatabaseMetaData getMetaData() throws SQLException { return new MySQLDatabaseMetaData(this,protocol.getUsername(), - "jdbc:mysql://" + protocol.getHost() + ":" + protocol.getPort() + "/" + protocol.getDatabase()); + "jdbc:mysql://" + protocol.getHost() + ":" + protocol.getPort() + "/" + protocol.getDatabase()); } /** @@ -274,7 +293,11 @@ public DatabaseMetaData getMetaData() throws SQLException { * @throws SQLException if there is a problem */ public void setReadOnly(final boolean readOnly) throws SQLException { - protocol.setReadonly(readOnly); + try { + protocol.setReadonly(readOnly); + } catch (QueryException e) { + SQLExceptionMapper.throwException(e, this, null); + } } /** @@ -285,25 +308,25 @@ public void setReadOnly(final boolean readOnly) throws SQLException { * connection */ public boolean isReadOnly() throws SQLException { - return false; + return protocol.getReadonly(); } public static String quoteIdentifier(String s) { - return "`" + s.replaceAll("`","``") + "`"; + return "`" + s.replaceAll("`","``") + "`"; } - + public static String unquoteIdentifier(String s) { - if (s != null && s.startsWith("`") && s.endsWith("`") && s.length()>= 2) { - return s.substring(1, s.length()-1).replace("``", "`"); - } - return s; + if (s != null && s.startsWith("`") && s.endsWith("`") && s.length()>= 2) { + return s.substring(1, s.length()-1).replace("``", "`"); + } + return s; } /** * Sets the given catalog name in order to select a subspace of this Connection object's database in * which to work. - * + * * If the driver does not support catalogs, it will silently ignore this request. - * + * * MySQL treats catalogs and databases as equivalent * * @param catalog the name of a catalog (subspace in this Connection object's database) in which to @@ -312,24 +335,21 @@ public static String unquoteIdentifier(String s) { * @see #getCatalog */ public void setCatalog(final String catalog) throws SQLException { - if (catalog == null){ - throw new SQLException("The catalog name may not be null", "XAE05"); - } - Statement st = createStatement(); + if (catalog == null){ + throw new SQLException("The catalog name may not be null", "XAE05"); + } try { - /* Quote modifiers correctly, with backtick char */ - st.execute("USE "+ quoteIdentifier(catalog) ); - st.close(); - } finally { - st.close(); + protocol.setCatalog(catalog); + } catch (QueryException e) { + SQLExceptionMapper.throwException(e, this, null); } } /** * Retrieves this Connection object's current catalog name. - * + * * catalogs are not supported in drizzle - * + * * TODO: Explain the wrapper interface to be able to change database * * @return the current catalog name or null if there is none @@ -337,7 +357,7 @@ public void setCatalog(final String catalog) throws SQLException { * @see #setCatalog */ public String getCatalog() throws SQLException { - String catalog = null; + String catalog = null; Statement st = null; try { st = createStatement(); @@ -354,7 +374,7 @@ public String getCatalog() throws SQLException { /** * Attempts to change the transaction isolation level for this Connection object to the one given. The * constants defined in the interface Connection are the possible transaction isolation levels. - * + * * Note: If this method is called during a transaction, the result is implementation-defined. * * @param level one of the following Connection constants: Connection.TRANSACTION_READ_UNCOMMITTED, @@ -367,31 +387,11 @@ public String getCatalog() throws SQLException { * @see #getTransactionIsolation */ public void setTransactionIsolation(final int level) throws SQLException { - String query = "SET SESSION TRANSACTION ISOLATION LEVEL"; - switch (level) { - case Connection.TRANSACTION_READ_UNCOMMITTED: - query += " READ UNCOMMITTED"; - break; - case Connection.TRANSACTION_READ_COMMITTED: - query += " READ COMMITTED"; - break; - case Connection.TRANSACTION_REPEATABLE_READ: - query += " REPEATABLE READ"; - break; - case Connection.TRANSACTION_SERIALIZABLE: - query += " SERIALIZABLE"; - break; - default: - throw SQLExceptionMapper.getSQLException("Unsupported transaction isolation level"); - } - - Statement st = createStatement(); try { - st.execute(query); - } finally { - st.close(); + protocol.setTransactionIsolation(level); + } catch (QueryException e) { + SQLExceptionMapper.throwException(e, this, null); } - } /** @@ -430,15 +430,15 @@ public int getTransactionIsolation() throws SQLException { /** * Not yet implemented: Protocol needs to store any warnings related to connections - * - * + * + * * Retrieves the first warning reported by calls on this Connection object. If there is more than one * warning, subsequent warnings will be chained to the first one and can be retrieved by calling the method * SQLWarning.getNextWarning on the warning that was retrieved previously. - * + * * This method may not be called on a closed connection; doing so will cause an SQLException to be * thrown. - * + * *

Note: Subsequent warnings will be chained to this SQLWarning. * * @return the first SQLWarning object or null if there are none @@ -446,7 +446,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; @@ -454,26 +454,26 @@ public SQLWarning getWarnings() throws SQLException { SQLWarning last = null; SQLWarning first = null; try { - st = this.createStatement(); - rs = st.executeQuery("show warnings"); - // returned result set has 'level', 'code' and 'message' columns, in this order. - while(rs.next()) { - int code = rs.getInt(2); - String message = rs.getString(3); - SQLWarning w = new SQLWarning(message, SQLExceptionMapper.mapMySQLCodeToSQLState(code), code); - if (first == null) { - first = w; - last = w; - } - else { - last.setNextWarning(w); - last = w; - } - } + st = this.createStatement(); + rs = st.executeQuery("show warnings"); + // returned result set has 'level', 'code' and 'message' columns, in this order. + while(rs.next()) { + int code = rs.getInt(2); + String message = rs.getString(3); + SQLWarning w = new SQLWarning(message, SQLExceptionMapper.mapMySQLCodeToSQLState(code), code); + if (first == null) { + first = w; + last = w; + } + else { + last.setNextWarning(w); + last = w; + } + } } finally { - if (rs != null) - rs.close(); + if (rs != null) + rs.close(); if(st != null) st.close(); } @@ -641,7 +641,7 @@ public int getHoldability() throws SQLException { /** * Creates an unnamed savepoint in the current transaction and returns the new Savepoint object that * represents it. - * + * * if setSavepoint is invoked outside of an active transaction, a transaction will be started at this newly * created savepoint. * @@ -661,7 +661,7 @@ public Savepoint setSavepoint() throws SQLException { /** * Creates a savepoint with the given name in the current transaction and returns the new Savepoint * object that represents it. - * + * * if setSavepoint is invoked outside of an active transaction, a transaction will be started at this newly * created savepoint. * @@ -685,7 +685,7 @@ public Savepoint setSavepoint(final String name) throws SQLException { /** * Undoes all changes made after the given Savepoint object was set. - * + * * This method should be used only when auto-commit has been disabled. * * @param savepoint the Savepoint object to roll back to @@ -700,9 +700,9 @@ public Savepoint setSavepoint(final String name) throws SQLException { * @since 1.4 */ public void rollback(final Savepoint savepoint) throws SQLException { - Statement st = createStatement(); - st.execute("ROLLBACK TO SAVEPOINT " + savepoint.toString()); - st.close(); + Statement st = createStatement(); + st.execute("ROLLBACK TO SAVEPOINT " + savepoint.toString()); + st.close(); } /** @@ -719,9 +719,9 @@ public void rollback(final Savepoint savepoint) throws SQLException { * @since 1.4 */ public void releaseSavepoint(final Savepoint savepoint) throws SQLException { - Statement st = createStatement(); - st.execute("RELEASE SAVEPOINT " + savepoint.toString()); - st.close(); + Statement st = createStatement(); + st.execute("RELEASE SAVEPOINT " + savepoint.toString()); + st.close(); } /** @@ -758,7 +758,7 @@ public Statement createStatement(final int resultSetType, final int resultSetCon /** * Creates a PreparedStatement object that will generate ResultSet objects with the given * type, concurrency, and holdability. - * + * * This method is the same as the prepareStatement method above, but it allows the default result set * type, concurrency, and holdability to be overridden. * @@ -828,13 +828,13 @@ public CallableStatement prepareCall(final String sql, * The given constant tells the driver whether it should make auto-generated keys available for retrieval. This * parameter is ignored if the SQL statement is not an INSERT statement, or an SQL statement able to * return auto-generated keys (the list of such statements is vendor-specific). - * + * * Note: This method is optimized for handling parametric SQL statements that benefit from precompilation. If * the driver supports precompilation, the method prepareStatement will send the statement to the * database for precompilation. Some drivers may not support precompilation. In this case, the statement may not be * sent to the database until the PreparedStatement object is executed. This has no direct effect on * users; however, it does affect which methods throw certain SQLExceptions. - * + * * Result sets created using the returned PreparedStatement object will by default be type * TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY. The holdability of * the created result sets can be determined by calling {@link #getHoldability}. @@ -862,16 +862,16 @@ public PreparedStatement prepareStatement(final String sql, final int autoGenera * auto-generated keys that should be made available. The driver will ignore the array if the SQL statement is not * an INSERT statement, or an SQL statement able to return auto-generated keys (the list of such * statements is vendor-specific). - * + * * An SQL statement with or without IN parameters can be pre-compiled and stored in a PreparedStatement * object. This object can then be used to efficiently execute this statement multiple times. - * + * * Note: This method is optimized for handling parametric SQL statements that benefit from precompilation. If * the driver supports precompilation, the method prepareStatement will send the statement to the * database for precompilation. Some drivers may not support precompilation. In this case, the statement may not be * sent to the database until the PreparedStatement object is executed. This has no direct effect on * users; however, it does affect which methods throw certain SQLExceptions. - * + * * Result sets created using the returned PreparedStatement object will by default be type * TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY. The holdability of * the created result sets can be determined by calling {@link #getHoldability}. @@ -896,16 +896,16 @@ public PreparedStatement prepareStatement(final String sql, final int[] columnIn * auto-generated keys that should be returned. The driver will ignore the array if the SQL statement is not an * INSERT statement, or an SQL statement able to return auto-generated keys (the list of such * statements is vendor-specific). - * + * * An SQL statement with or without IN parameters can be pre-compiled and stored in a PreparedStatement * object. This object can then be used to efficiently execute this statement multiple times. - * + * * Note: This method is optimized for handling parametric SQL statements that benefit from precompilation. If * the driver supports precompilation, the method prepareStatement will send the statement to the * database for precompilation. Some drivers may not support precompilation. In this case, the statement may not be * sent to the database until the PreparedStatement object is executed. This has no direct effect on * users; however, it does affect which methods throw certain SQLExceptions. - * + * * Result sets created using the returned PreparedStatement object will by default be type * TYPE_FORWARD_ONLY and have a concurrency level of CONCUR_READ_ONLY. The holdability of * the created result sets can be determined by calling {@link #getHoldability}. @@ -996,75 +996,75 @@ public java.sql.SQLXML createSQLXML() throws SQLException { * Returns true if the connection has not been closed and is still valid. The driver shall submit a query on the * connection or use some other mechanism that positively verifies the connection is still valid when this method is * called. - * + * * The query submitted by the driver to validate the connection shall be executed in the context of the current * transaction. * * @param timeout - The time in seconds to wait for the database operation used to validate the * connection to complete. If the timeout period expires before the operation completes, this method * returns false. A value of 0 indicates a timeout is not applied to the database operation. - * + * * @return true if the connection is valid, false otherwise * @throws java.sql.SQLException if the value supplied for timeout is less then 0 * @see java.sql.DatabaseMetaData#getClientInfoProperties * @since 1.6 - * + * */ public boolean isValid(final int timeout) throws SQLException { - if (timeout < 0) { - throw new SQLException("the value supplied for timeout is negative"); - } - if (isClosed()) { - return false; - } + if (timeout < 0) { + throw new SQLException("the value supplied for timeout is negative"); + } + if (isClosed()) { + return false; + } try { return protocol.ping(); } catch (QueryException e) { - return false; + return false; } } /** * Sets the value of the client info property specified by name to the value specified by value. - * + * * Applications may use the DatabaseMetaData.getClientInfoProperties method to determine the client * info properties supported by the driver and the maximum length that may be specified for each property. - * + * * The driver stores the value specified in a suitable location in the database. For example in a special register, * session parameter, or system table column. For efficiency the driver may defer setting the value in the database * until the next time a statement is executed or prepared. Other than storing the client information in the * appropriate place in the database, these methods shall not alter the behavior of the connection in anyway. The * values supplied to these methods are used for accounting, diagnostics and debugging purposes only. - * + * * The driver shall generate a warning if the client info name specified is not recognized by the driver. - * + * * If the value specified to this method is greater than the maximum length for the property the driver may either * truncate the value and generate a warning or generate a SQLClientInfoException. If the driver * generates a SQLClientInfoException, the value specified was not set on the connection. - * + * * The following are standard client info properties. Drivers are not required to support these properties however * if the driver supports a client info property that can be described by one of the standard properties, the * standard property name should be used. - * + * *

  • ApplicationName - The name of the application currently utilizing the connection
  • *
  • ClientUser - The name of the user that the application using the connection is performing * work for. This may not be the same as the user name that was used in establishing the connection.
  • *
  • ClientHostname - The hostname of the computer the application using the connection is running * on.
- * + * * * @param name The name of the client info property to set * @param value The value to set the client info property to. If the value is null, the current value of the * specified property is cleared. - * + * * @throws java.sql.SQLClientInfoException * if the database server returns an error while setting the client info value on the database server or * this method is called on a closed connection - * + * * @since 1.6 */ public void setClientInfo(final String name, final String value) throws java.sql.SQLClientInfoException { - this.clientInfoProperties.setProperty(name, value); + DefaultOptions.addProperty(protocol.getJdbcUrl().getHaMode(), name, value, this.options); } /** @@ -1074,79 +1074,76 @@ public void setClientInfo(final String name, final String value) throws java.sql * currently set on the connection is not present in the properties list, that property is cleared. Specifying an * empty properties list will clear all of the properties on the connection. See setClientInfo (String, * String) for more information. - * + * * If an error occurs in setting any of the client info properties, a SQLClientInfoException is thrown. * The SQLClientInfoException contains information indicating which client info properties were not * set. The state of the client information is unknown because some databases do not allow multiple client info * properties to be set atomically. For those databases, one or more properties may have been set before the error * occurred. - * + * * * @param properties the list of client info properties to set - * + * * @throws java.sql.SQLClientInfoException * if the database server returns an error while setting the clientInfo values on the database server or * this method is called on a closed connection - * + * * @see java.sql.Connection#setClientInfo(String, String) setClientInfo(String, String) * @since 1.6 - * + * */ public void setClientInfo(final Properties properties) throws java.sql.SQLClientInfoException { - // TODO: actually use these! - for (final String key : properties.stringPropertyNames()) { - this.clientInfoProperties.setProperty(key, properties.getProperty(key)); - } + DefaultOptions.addProperty(protocol.getJdbcUrl().getHaMode(), properties, this.options); } /** * Returns the value of the client info property specified by name. This method may return null if the specified * client info property has not been set and does not have a default value. This method will also return null if * the specified client info property name is not supported by the driver. - * + * * Applications may use the DatabaseMetaData.getClientInfoProperties method to determine the client * info properties supported by the driver. - * + * * * @param name The name of the client info property to retrieve - * + * * @return The value of the client info property specified - * + * * @throws java.sql.SQLException if the database server returns an error when fetching the client info value from * the database or this method is called on a closed connection - * + * * @see java.sql.DatabaseMetaData#getClientInfoProperties * @since 1.6 - * + * */ public String getClientInfo(final String name) throws SQLException { - return clientInfoProperties.getProperty(name); + return DefaultOptions.getProperties(name, options); } /** * Returns a list containing the name and current value of each client info property supported by the driver. The * value of a client info property may be null if the property has not been set and does not have a default value. - * + * * * @return A Properties object that contains the name and current value of each of the client info * properties supported by the driver. - * + * * @throws java.sql.SQLException if the database server returns an error when fetching the client info values from * the database or this method is called on a closed connection - * + * * @since 1.6 */ public Properties getClientInfo() throws SQLException { - return clientInfoProperties; + return DefaultOptions.getProperties(options); } /** * Factory method for creating Array objects. - * + * * Note: When createArrayOf is used to create an array object that maps to a primitive data * type, then it is implementation-defined whether the Array object is an array of that primitive data * type or an array of Object. - * + * * Note: The JDBC driver is responsible for mapping the elements Object array to the default * JDBC SQL type defined in java.sql.Types for the given class of Object. The default mapping is * specified in Appendix B of the JDBC specification. If the resulting JDBC type is not the appropriate type for @@ -1190,7 +1187,7 @@ public Struct createStruct(final String typeName, final Object[] attributes) thr /** * Returns an object that implements the given interface to allow access to non-standard methods, or standard * methods not exposed by the proxy. - * + * * If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the * receiver is a wrapper and the wrapped object implements the interface then the result is the wrapped object or a * proxy for the wrapped object. Otherwise return the the result of calling unwrap recursively on the @@ -1269,14 +1266,13 @@ public int getPort() { public String getDatabase() { return protocol.getDatabase(); } - - public String getPinGlobalTxToPhysicalConnection() { - return protocol.getPinGlobalTxToPhysicalConnection(); + + protected boolean getPinGlobalTxToPhysicalConnection() { + return protocol.getPinGlobalTxToPhysicalConnection(); } - public void setHostFailed() { - protocol.setHostFailed(); + if (protocol.getProxy() == null) protocol.setHostFailedWithoutProxy(); } volatile int lowercaseTableNames = -1; @@ -1289,66 +1285,66 @@ public int getLowercaseTableNames() throws SQLException { } return lowercaseTableNames; } - - /* (non-Javadoc) - * @see java.sql.Connection#abort(java.util.concurrent.Executor) - */ - public void abort(Executor executor) throws SQLException { - if (this.isClosed()) { - return; - } - SQLPermission sqlPermission = new SQLPermission("callAbort"); + + /* (non-Javadoc) + * @see java.sql.Connection#abort(java.util.concurrent.Executor) + */ + public void abort(Executor executor) throws SQLException { + if (this.isClosed()) { + return; + } + SQLPermission sqlPermission = new SQLPermission("callAbort"); SecurityManager securityManager = System.getSecurityManager(); - if (securityManager != null && sqlPermission != null) { - securityManager.checkPermission(sqlPermission); - } - if (executor == null) { - throw SQLExceptionMapper.getSQLException("Cannot abort the connection: null executor passed"); - } - executor.execute(new Runnable() { - @Override - public void run() { - try { - close(); - pooledConnection = null; - } catch (SQLException sqle) { - throw new RuntimeException(sqle); - } - } - }); - } - - public int getNetworkTimeout() throws SQLException { - try { - return this.protocol.getTimeout(); - } catch (SocketException se) { - throw SQLExceptionMapper.getSQLException("Cannot retrieve the network timeout", se); - } - } - - public String getSchema() throws SQLException { - // We support only catalog - return null; - } - - /* (non-Javadoc) - * @see java.sql.Connection#setNetworkTimeout(java.util.concurrent.Executor, int) - */ - public void setNetworkTimeout(Executor executor, final int milliseconds) throws SQLException { - if (this.isClosed()) { - throw SQLExceptionMapper.getSQLException("Connection.setNetworkTimeout cannot be called on a closed connection"); - } - if (milliseconds < 0) { - throw SQLExceptionMapper.getSQLException("Connection.setNetworkTimeout cannot be called with a negative timeout"); - } - SQLPermission sqlPermission = new SQLPermission("setNetworkTimeout"); + if (securityManager != null && sqlPermission != null) { + securityManager.checkPermission(sqlPermission); + } + if (executor == null) { + throw SQLExceptionMapper.getSQLException("Cannot abort the connection: null executor passed"); + } + executor.execute(new Runnable() { + @Override + public void run() { + try { + close(); + pooledConnection = null; + } catch (SQLException sqle) { + throw new RuntimeException(sqle); + } + } + }); + } + + public int getNetworkTimeout() throws SQLException { + try { + return this.protocol.getTimeout(); + } catch (SocketException se) { + throw SQLExceptionMapper.getSQLException("Cannot retrieve the network timeout", se); + } + } + + public String getSchema() throws SQLException { + // We support only catalog + return null; + } + + /* (non-Javadoc) + * @see java.sql.Connection#setNetworkTimeout(java.util.concurrent.Executor, int) + */ + public void setNetworkTimeout(Executor executor, final int milliseconds) throws SQLException { + if (this.isClosed()) { + throw SQLExceptionMapper.getSQLException("Connection.setNetworkTimeout cannot be called on a closed connection"); + } + if (milliseconds < 0) { + throw SQLExceptionMapper.getSQLException("Connection.setNetworkTimeout cannot be called with a negative timeout"); + } + SQLPermission sqlPermission = new SQLPermission("setNetworkTimeout"); SecurityManager securityManager = System.getSecurityManager(); - if (securityManager != null && sqlPermission != null) { - securityManager.checkPermission(sqlPermission); - } - if (executor == null) { - throw SQLExceptionMapper.getSQLException("Cannot set the connection timeout: null executor passed"); - } + if (securityManager != null && sqlPermission != null) { + securityManager.checkPermission(sqlPermission); + } + if (executor == null) { + throw SQLExceptionMapper.getSQLException("Cannot set the connection timeout: null executor passed"); + } // executor.execute(new Runnable() { // @Override // public void run() { @@ -1359,15 +1355,16 @@ public void setNetworkTimeout(Executor executor, final int milliseconds) throws // } // } // }); - try { - protocol.setTimeout(milliseconds); - } catch (SocketException se) { - throw SQLExceptionMapper.getSQLException("Cannot set the network timeout", se); - } - } - - public void setSchema(String arg0) throws SQLException { - // We support only catalog - throw SQLExceptionMapper.getFeatureNotSupportedException("Only catalogs are supported"); - } + try { + protocol.setTimeout(milliseconds); + } catch (SocketException se) { + throw SQLExceptionMapper.getSQLException("Cannot set the network timeout", se); + } + } + + public void setSchema(String arg0) throws SQLException { + // We support only catalog + throw SQLExceptionMapper.getFeatureNotSupportedException("Only catalogs are supported"); + } + } diff --git a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java b/src/main/java/org/mariadb/jdbc/MySQLDataSource.java index 6dca50afb..190c91553 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLDataSource.java +++ b/src/main/java/org/mariadb/jdbc/MySQLDataSource.java @@ -51,243 +51,197 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS package org.mariadb.jdbc; import org.mariadb.jdbc.internal.SQLExceptionMapper; +import org.mariadb.jdbc.internal.common.DefaultOptions; import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.UrlHAMode; 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; +import java.util.ArrayList; import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Logger; public class MySQLDataSource implements DataSource, ConnectionPoolDataSource, XADataSource { + private final static Logger log = Logger.getLogger(MySQLDataSource.class.getName()); - - private String hostname = "localhost"; - private int port = 0; - private String database = ""; - private String username = null; - private String password = null; - private Properties info = null; - private JDBCUrl url; + private final JDBCUrl jdbcUrl; public MySQLDataSource(String hostname, int port, String database) { - this.hostname = hostname; - this.port = port; - this.database = database; - this.info = new Properties(); + ArrayList hostAddresses = new ArrayList(); + hostAddresses.add(new HostAddress(hostname, port)); + jdbcUrl = new JDBCUrl(database, hostAddresses, DefaultOptions.defaultValues(UrlHAMode.NONE), UrlHAMode.NONE); + } + + public MySQLDataSource(String url) { + this.jdbcUrl = JDBCUrl.parse(url); } public MySQLDataSource() { - this.info = new Properties(); + ArrayList hostAddresses = new ArrayList(); + hostAddresses.add(new HostAddress("localhost", 3306)); + jdbcUrl = new JDBCUrl("", hostAddresses, DefaultOptions.defaultValues(UrlHAMode.NONE), UrlHAMode.NONE); } /** * Sets the database name. - * + * * @param dbName * the name of the database */ public void setDatabaseName(String dbName) { - this.database = dbName; - resetUrl(); + jdbcUrl.setDatabase(dbName); } /** * Gets the name of the database - * + * * @return the name of the database for this data source */ public String getDatabaseName() { - return (this.database != null) ? this.database : ""; + return (jdbcUrl.getDatabase() != null) ? jdbcUrl.getDatabase() : ""; } /** * Sets the username - * + * * @param userName * the username */ public void setUser(String userName) { - setUserName(userName); + setUserName(userName); } /** * Gets the username - * + * * @return the username to use when connecting to the database */ public String getUser() { - return this.username; + return jdbcUrl.getUsername(); } /** * Sets the username - * + * * @param userName * the username */ public void setUserName(String userName) { - this.username = userName; + jdbcUrl.setUsername(userName); } /** * Gets the username - * + * * @return the username to use when connecting to the database */ public String getUserName() { - return this.username; + return jdbcUrl.getUsername(); } /** * Sets the password - * + * * @param pass * the password */ public void setPassword(String pass) { - this.password = pass; + jdbcUrl.setPassword(pass); } /** * Sets the database port. - * + * * @param p * the port */ public void setPort(int p) { - this.port = p; - resetUrl(); + jdbcUrl.getHostAddresses().get(0).port = p; } /** * Returns the port number - * + * * @return the port number */ public int getPort() { - return this.port; + return jdbcUrl.getHostAddresses().get(0).port; } /** * Sets the port number - * + * * @param p * the port - * + * * @see #setPort */ public void setPortNumber(int p) { - setPort(p); + setPort(p); } /** * Returns the port number - * + * * @return the port number */ public int getPortNumber() { - return getPort(); + return getPort(); } /** * Sets the server name. - * + * * @param serverName * the server name */ public void setServerName(String serverName) { - this.hostname = serverName; - resetUrl(); + jdbcUrl.getHostAddresses().get(0).host = serverName; } public void setProperties(String properties) { - Utils.setUrlParameters(properties, this.info); + jdbcUrl.setProperties(properties); } /** * Sets the connection string URL. - * + * * @param url * the connection string */ public void setURL(String url) { - setUrl(url); + setUrl(url); } /** * Sets the connection string URL. - * + * * @param s * the connection string */ public void setUrl(String s) { - - String baseUrl = s; - int idx = s.lastIndexOf("?"); - if (idx > 0) { - baseUrl = s.substring(0,idx); - String urlParams = s.substring(idx+1); - setProperties(urlParams); - } - this.url = JDBCUrl.parse(baseUrl); - - String tmpStr; - if ((tmpStr = url.getDatabase()) != null) { - this.database = tmpStr; - } - if ((tmpStr = url.getHostname()) != null) { - this.hostname = tmpStr; - } - tmpStr = url.getUsername(); - if (tmpStr.equals("")) { - tmpStr = this.info.getProperty("user", ""); - } - if (!tmpStr.equals("")) { - this.username = tmpStr; - } - tmpStr = url.getPassword(); - if (tmpStr.equals("")) { - tmpStr = this.info.getProperty("password", ""); - } - if (!tmpStr.equals("")) { - this.password = tmpStr; - } - this.port = url.getPort(); + this.jdbcUrl.parseUrl(s); } /** * Returns the name of the database server - * + * * @return the name of the database server */ public String getServerName() { - return (this.hostname != null) ? this.hostname : ""; + return (this.jdbcUrl.getHostAddresses().get(0).host != null) ? this.jdbcUrl.getHostAddresses().get(0).host : ""; } - - void createUrl() { - if (url != null) - return; - - String urlString = "jdbc:mysql://" + hostname; - if (port != 0) - urlString = urlString + ":" + port; - if (database != null) - urlString = urlString + "/" + database; - url = JDBCUrl.parse(urlString); - } - - private void resetUrl() { - this.url = null; - } /** * Attempts to establish a connection with the data source that this DataSource object represents. * @@ -295,9 +249,10 @@ private void resetUrl() { * @throws java.sql.SQLException if a database access error occurs */ public Connection getConnection() throws SQLException { - createUrl(); try { - return MySQLConnection.newConnection(new MySQLProtocol(url, username, password, info)); + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + Protocol proxyfiedProtocol = Utils.retrieveProxy(jdbcUrl, lock); + return MySQLConnection.newConnection(proxyfiedProtocol, lock); } catch (QueryException e) { SQLExceptionMapper.throwException(e, null, null); return null; @@ -314,19 +269,15 @@ public Connection getConnection() throws SQLException { * @since 1.4 */ public Connection getConnection(final String username, final String password) throws SQLException { - createUrl(); - try { - Properties props = info == null ? new Properties() : info; - return MySQLConnection.newConnection(new MySQLProtocol(url, username, password, props)); - } catch (QueryException e) { - SQLExceptionMapper.throwException(e, null, null); - return null; - } + jdbcUrl.setUsername(username); + jdbcUrl.setPassword(password); + log.finest("connection : " +jdbcUrl.toString()); + return getConnection(); } /** * Retrieves the log writer for this DataSource object. - * + * * The log writer is a character output stream to which all logging and tracing messages for this data source * will be printed. This includes messages printed by the methods of this object, messages printed by methods of * other objects manufactured by this object, and so on. Messages printed to a data source specific log writer are @@ -346,7 +297,7 @@ public PrintWriter getLogWriter() throws SQLException { /** * Sets the log writer for this DataSource object to the given java.io.PrintWriter * object. - * + * * The log writer is a character output stream to which all logging and tracing messages for this data source * will be printed. This includes messages printed by the methods of this object, messages printed by methods of * other objects manufactured by this object, and so on. Messages printed to a data source- specific log writer are @@ -394,7 +345,7 @@ public int getLoginTimeout() throws SQLException { /** * Returns an object that implements the given interface to allow access to non-standard methods, or standard * methods not exposed by the proxy. - * + * * If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the * receiver is a wrapper and the wrapped object implements the interface then the result is the wrapped object or a * proxy for the wrapped object. Otherwise return the the result of calling unwrap recursively on the @@ -458,7 +409,7 @@ public PooledConnection getPooledConnection() throws SQLException { * @since 1.4 */ public PooledConnection getPooledConnection(String user, String password) throws SQLException { - return new MySQLPooledConnection((MySQLConnection)getConnection(user,password)); + return new MySQLPooledConnection((MySQLConnection)getConnection(user,password)); } public XAConnection getXAConnection() throws SQLException { @@ -468,8 +419,8 @@ public XAConnection getXAConnection(String user, String password) throws SQLExce return new MySQLXAConnection((MySQLConnection)getConnection(user,password)); } - public Logger getParentLogger() throws SQLFeatureNotSupportedException { - // TODO Auto-generated method stub - return null; - } + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + // TODO Auto-generated method stub + return null; + } } diff --git a/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java b/src/main/java/org/mariadb/jdbc/MySQLDatabaseMetaData.java index d057d685e..945013628 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 "; } @@ -492,7 +492,7 @@ public String getDriverName() throws SQLException { } public String getDriverVersion() throws SQLException { - return Version.pomversion; + return String.format("%d.%d",getDriverMajorVersion(),getDriverMinorVersion()); } diff --git a/src/main/java/org/mariadb/jdbc/MySQLPreparedStatement.java b/src/main/java/org/mariadb/jdbc/MySQLPreparedStatement.java index 63a15d02a..87428fd35 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLPreparedStatement.java +++ b/src/main/java/org/mariadb/jdbc/MySQLPreparedStatement.java @@ -81,7 +81,7 @@ public MySQLPreparedStatement(MySQLConnection connection, super(connection); this.sql = sql; useFractionalSeconds = - connection.getProtocol().getInfo().getProperty("useFractionalSeconds") != null; + connection.getProtocol().getOptions().useFractionalSeconds; if(log.isLoggable(Level.FINEST)) { log.finest("Creating prepared statement for " + sql); } @@ -1513,16 +1513,21 @@ public void setBigDecimal(final int parameterIndex, final BigDecimal x) throws S // Close prepared statement, maybe fire closed-statement events @Override - public synchronized void close() throws SQLException { - super.close(); + public void close() throws SQLException { + connection.lock.writeLock().lock(); + try { + super.close(); - if (connection == null || connection.pooledConnection == null || - connection.pooledConnection.statementEventListeners.isEmpty()) { - return; - } + if (connection == null || connection.pooledConnection == null || + connection.pooledConnection.statementEventListeners.isEmpty()) { + return; + } - isClosed = false; - connection.pooledConnection.fireStatementClosed(this); + isClosed = false; + connection.pooledConnection.fireStatementClosed(this); + } finally { + connection.lock.writeLock().unlock(); + } } public String toString() { diff --git a/src/main/java/org/mariadb/jdbc/MySQLResultSet.java b/src/main/java/org/mariadb/jdbc/MySQLResultSet.java index 5c487658a..b678669ab 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; @@ -406,13 +403,7 @@ public String getCursorName() throws SQLException { * @throws java.sql.SQLException if a database access error occurs or this method is called on a closed result set */ public ResultSetMetaData getMetaData() throws SQLException { - boolean returnTableAlias = false; - - if (protocol.getInfo().getProperty("useOldAliasMetadataBehavior") != null - && "true".equalsIgnoreCase(protocol.getInfo().getProperty("useOldAliasMetadataBehavior"))) - returnTableAlias = true; - - return new MySQLResultSetMetaData(queryResult.getColumnInformation(), protocol.datatypeMappingFlags, returnTableAlias); + return new MySQLResultSetMetaData(queryResult.getColumnInformation(), protocol.getDatatypeMappingFlags(), protocol.getOptions().useOldAliasMetadataBehavior); } /** @@ -447,7 +438,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 +3744,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 +3801,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 +3816,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 +3866,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..9c2fd19d1 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,24 +28,23 @@ 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) { + connection.lock.writeLock().lock(); + try { 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"); } result = protocol.prepare(sql); + } finally { + connection.lock.writeLock().unlock(); } - - if (protocol.getInfo().getProperty("useOldAliasMetadataBehavior") != null - && "true".equalsIgnoreCase(protocol.getInfo().getProperty( - "useOldAliasMetadataBehavior"))) - returnTableAlias = true; + returnTableAlias = protocol.getOptions().useOldAliasMetadataBehavior; metadata = new MySQLResultSetMetaData(result.columns, - protocol.datatypeMappingFlags, returnTableAlias); + protocol.getDatatypeMappingFlags(), returnTableAlias); parameterInfo = result.parameters; statementId = result.statementId; } catch (QueryException e) { @@ -148,6 +148,7 @@ public void setAsciiStream(int parameterIndex, InputStream x, int length) throw new UnsupportedOperationException("Not supported yet."); } + @Deprecated @Override public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { diff --git a/src/main/java/org/mariadb/jdbc/MySQLStatement.java b/src/main/java/org/mariadb/jdbc/MySQLStatement.java index d7c4dd056..f76d2eb72 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLStatement.java +++ b/src/main/java/org/mariadb/jdbc/MySQLStatement.java @@ -57,7 +57,7 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.common.queryresults.ModifyQueryResult; 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,13 +69,12 @@ public class MySQLStatement implements Statement { /** * the protocol used to talk to the server. */ - private final MySQLProtocol protocol; + private final Protocol protocol; /** * the Connection object. */ protected MySQLConnection connection; - /** * The actual query result. */ @@ -89,6 +88,7 @@ public class MySQLStatement implements Statement { private int queryTimeout; private boolean escapeProcessing; private int fetchSize; + private boolean wasStreaming=false; private int maxRows; boolean isClosed; private static volatile Timer timer; @@ -116,7 +116,7 @@ public MySQLStatement(MySQLConnection connection) { this.protocol = connection.getProtocol(); this.connection = connection; this.escapeProcessing = true; - cachedResultSets = new LinkedList(); + cachedResultSets = new LinkedList<>(); } @@ -125,7 +125,7 @@ public MySQLStatement(MySQLConnection connection) { * * @return the protocol used. */ - public MySQLProtocol getProtocol() { + public Protocol getProtocol() { return protocol; } @@ -141,90 +141,71 @@ private static Timer getTimer() { } return result; } - + // Part of query prolog - setup timeout timer private void setTimerTask() { - assert(timerTask == null); + assert(timerTask == null); timerTask = new TimerTask() { - @Override - public void run() { - try { - isTimedout = true; - protocol.cancelCurrentQuery(); - } catch (Throwable e) { - } - } - }; - 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 - } - } - } - + @Override + public void run() { + try { + isTimedout = true; + protocol.cancelCurrentQuery(); + } catch (Throwable e) { + } + } + }; + getTimer().schedule(timerTask, queryTimeout*1000); + } + 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"); - } - 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.isExplicitClosed()) { + throw new SQLException("execute() is called on closed connection"); } - if (protocol.hasMoreResults()) { - // Skip remaining result sets. CallableStatement might return many of them - - // not only the "select" result sets, but also the "update" results + //old failover handling + if (protocol.getProxy() == null) checkReconnectWithoutProxy(); + + protocol.closeIfActiveResult(); + if (wasStreaming && protocol.hasMoreResults()) { + // Skip remaining result sets. CallableStatement might return many of them - + // not only the "select" result sets, but also the "update" results while(getMoreResults(true)) { } } cachedResultSets.clear(); - MySQLConnection conn = (MySQLConnection)getConnection(); - conn.reenableWarnings(); - + ((MySQLConnection)getConnection()).reenableWarnings(); + try { protocol.setMaxRows(maxRows); } catch(QueryException qe) { SQLExceptionMapper.throwException(qe, connection, this); } - + if (queryTimeout != 0) { - setTimerTask(); + setTimerTask(); } } private void cacheMoreResults() { + wasStreaming = isStreaming(); + if (isStreaming()) return; - if (isStreaming()) - return; QueryResult saveResult = queryResult; for(;;) { try { - if (protocol.hasMoreResults()) { - getMoreResults(false); - cachedResultSets.add(queryResult); + if (protocol.hasMoreResults()) { + getMoreResults(false); + cachedResultSets.add(queryResult); } else { break; } } catch(SQLException e) { - cachedResultSets.add(e); - break; + cachedResultSets.add(e); + break; } } queryResult = saveResult; @@ -238,17 +219,17 @@ private void cacheMoreResults() { private void executeQueryEpilog(QueryException e, Query query) throws SQLException{ if (timerTask != null) { - timerTask.cancel(); - timerTask = null; + timerTask.cancel(); + timerTask = null; } if (isTimedout) { isTimedout = false; e = new QueryException("Query timed out", 1317, "JZ0002", e); } - + if (e == null) - return; + return; /* Include query into exception message, if dumpQueriesOnException is true, * or on SQL syntax error (MySQL error code 1064). @@ -256,16 +237,19 @@ private void executeQueryEpilog(QueryException e, Query query) throws SQLExcepti * If SQL query is too long, truncate it to reasonable (for exception messages) * length. */ - if (protocol.getInfo().getProperty("dumpQueriesOnException", "false").equalsIgnoreCase("true") - || e.getErrorCode() == 1064 ) { - String queryString = query.toString(); + if (protocol.getOptions().dumpQueriesOnException + || e.getErrorCode() == 1064 ) { + String queryString = query.toString(); if (queryString.length() > 4096) { - queryString = queryString.substring(0, 4096); + queryString = queryString.substring(0, 4096); } e.setMessage(e.getMessage()+ "\nQuery is:\n" + queryString); } - - SQLExceptionMapper.throwException(e, connection, this); + + //if has a failover, closing the statement + if (e.getSqlState() != null && e.getSqlState().startsWith("08")) close(); + + SQLExceptionMapper.throwException(e, connection, this); } /** @@ -273,29 +257,29 @@ private void executeQueryEpilog(QueryException e, Query query) throws SQLExcepti * * @param query the query * @return true if there was a result set, false otherwise. - * @throws SQLException + * @throws SQLException the error description */ - protected boolean execute(Query query) throws SQLException { - //System.out.println(query); - synchronized (protocol) { - if (protocol.activeResult != null) { - protocol.activeResult.close(); - } - executing = true; - QueryException exception = null; + protected boolean execute(Query query) throws SQLException { + + executing = true; + QueryException exception = null; + connection.lock.writeLock().lock(); + try { executeQueryProlog(); try { - batchResultSet = null; + batchResultSet = null; queryResult = protocol.executeQuery(query, isStreaming()); cacheMoreResults(); return (queryResult.getResultSetType() == ResultSetType.SELECT); } catch (QueryException e) { - exception = e; - return false; + exception = e; + return false; } finally { executeQueryEpilog(exception, query); executing = false; } + } finally { + connection.lock.writeLock().unlock(); } } @@ -312,16 +296,14 @@ protected boolean execute(Query query) throws SQLException { * @param isRewritable are the queries of the same type to be agreggated * @param rewriteOffset offset of the parameter if query are similar * @return true if there was a result set, false otherwise. - * @throws SQLException + * @throws SQLException the error description */ protected boolean execute(List queries, boolean isRewritable, int rewriteOffset) throws SQLException { - //System.out.println(query); - synchronized (protocol) { - if (protocol.activeResult != null) { - protocol.activeResult.close(); - } - executing = true; - QueryException exception = null; + + executing = true; + QueryException exception = null; + connection.lock.writeLock().lock(); + try { executeQueryProlog(); try { batchResultSet = null; @@ -335,6 +317,8 @@ protected boolean execute(List queries, boolean isRewritable, int rewrite executeQueryEpilog(exception, queries.get(0)); executing = false; } + } finally { + connection.lock.writeLock().unlock(); } } @@ -397,7 +381,7 @@ public int executeUpdate(String queryString) throws SQLException { } - /** + /** * executes a select query. * * @param queryString the query to send to the server @@ -413,9 +397,9 @@ public ResultSet executeQuery(String queryString) throws SQLException { * Releases this Statement object's database and JDBC resources immediately instead of waiting for this * to happen when it is automatically closed. It is generally good practice to release resources as soon as you are * finished with them to avoid tying up database resources. - * + * * Calling the method close on a Statement object that is already closed has no effect. - * + * * Note:When a Statement object is closed, its current ResultSet object, if one * exists, is also closed. * @@ -432,10 +416,12 @@ public void close() throws SQLException { // immediately garbage collected cachedResultSets.clear(); if (isStreaming()) { - synchronized (protocol) { - // Skip all outstanding result sets - while(getMoreResults(true)) { + connection.lock.writeLock().lock(); + try { + while (getMoreResults(true)) { } + } finally { + connection.lock.writeLock().unlock(); } } isClosed = true; @@ -461,7 +447,7 @@ public int getMaxFieldSize() throws SQLException { /** * Sets the limit for the maximum number of bytes that can be returned for character and binary column values in a * ResultSet object produced by this Statement object. - * + * * This limit applies only to BINARY, VARBINARY, LONGVARBINARY, * CHAR, VARCHAR, NCHAR, NVARCHAR, LONGNVARCHAR and * LONGVARCHAR fields. If the limit is exceeded, the excess data is silently discarded. For maximum @@ -510,7 +496,7 @@ public void setMaxRows(final int max) throws SQLException { /** * Sets escape processing on or off. If escape scanning is on (the default), the driver will do escape substitution * before sending the SQL statement to the database. - * + * * Note: Since prepared statements have usually been parsed prior to making this call, disabling escape processing * for PreparedStatements objects will have no effect. * @@ -555,13 +541,13 @@ public void setQueryTimeout(final int seconds) throws SQLException { * Sets the inputStream that will be used for the next execute that uses * "LOAD DATA LOCAL INFILE". The name specified as local file/URL will be * ignored. - * + * * @param inputStream inputStream instance, that will be used to send data to server */ public void setLocalInfileInputStream(InputStream inputStream) { - protocol.setLocalInfileInputStream(inputStream); + protocol.setLocalInfileInputStream(inputStream); } - + /** * Cancels this Statement object if both the DBMS and driver support aborting an SQL statement. This * method can be used by one thread to cancel a statement that is being executed by another thread. @@ -589,10 +575,10 @@ public void cancel() throws SQLException { /** * Retrieves the first warning reported by calls on this Statement object. Subsequent * Statement object warnings will be chained to this SQLWarning object. - * + * * The warning chain is automatically cleared each time a statement is (re)executed. This method may not be * called on a closed Statement object; doing so will cause an SQLException to be thrown. - * + * *

Note: If you are processing a ResultSet object, any warnings associated with reads on that * ResultSet object will be chained on it rather than on the Statement object that * produced it. @@ -628,7 +614,7 @@ public void clearWarnings() throws SQLException { * cursor has the proper isolation level to support updates, the cursor's SELECT statement should have * the form SELECT FOR UPDATE. If FOR UPDATE is not present, positioned updates may * fail. - * + * *

Note: By definition, the execution of positioned updates and deletes must be done by a different * Statement object than the one that generated the ResultSet object being used for * positioning. Also, cursor names must be unique within a connection. @@ -657,10 +643,10 @@ public Connection getConnection() throws SQLException { * Moves to this Statement object's next result, deals with any current ResultSet * object(s) according to the instructions specified by the given flag, and returns true if the next * result is a ResultSet object. - * + * * There are no more results when the following is true:

 // stmt is a Statement object
      * ((stmt.getMoreResults(current) == false) && (stmt.getUpdateCount() == -1))
- * + * * * @param current one of the following Statement constants indicating what should happen to current * ResultSet objects obtained using the method getResultSet: @@ -686,7 +672,7 @@ public boolean getMoreResults(final int current) throws SQLException { /** * Retrieves any auto-generated keys created as a result of executing this Statement object. If this * Statement object did not generate any keys, an empty ResultSet object is returned. - * + * * Note:If the columns which represent the auto-generated keys were not specified, the JDBC driver * implementation will determine the columns which best represent the auto-generated keys. * @@ -699,16 +685,16 @@ public boolean getMoreResults(final int current) throws SQLException { * @since 1.4 */ public ResultSet getGeneratedKeys() throws SQLException { - if (batchResultSet != null) { - return batchResultSet; - } + if (batchResultSet != null) { + return batchResultSet; + } if (queryResult != null && queryResult.getResultSetType() == ResultSetType.MODIFY) { long insertId = ((ModifyQueryResult)queryResult).getInsertId(); if (insertId == 0) { - return MySQLResultSet.createEmptyGeneratedKeysResultSet(connection); + return MySQLResultSet.createEmptyGeneratedKeysResultSet(connection); } int updateCount = getUpdateCount(); - + return MySQLResultSet.createGeneratedKeysResultSet(insertId, updateCount, connection); } return MySQLResultSet.EMPTY; @@ -763,7 +749,7 @@ public int executeUpdate(final String sql, final int autoGeneratedKeys) throws S * @since 1.4 */ public int executeUpdate(final String sql, final int[] columnIndexes) throws SQLException { - return executeUpdate(sql); + return executeUpdate(sql); } /** @@ -796,11 +782,11 @@ public int executeUpdate(final String sql, final String[] columnNames) throws SQ * auto-generated keys should be made available for retrieval. The driver will ignore this signal if the SQL * statement is not an INSERT statement, or an SQL statement able to return auto-generated keys (the * list of such statements is vendor-specific). - * + * * In some (uncommon) situations, a single SQL statement may return multiple result sets and/or update counts. * Normally you can ignore this unless you are (1) executing a stored procedure that you know may return multiple * results or (2) you are dynamically executing an unknown SQL string. - * + * * The execute method executes an SQL statement and indicates the form of the first result. You must * then use the methods getResultSet or getUpdateCount to retrieve the result, and * getMoreResults to move to any subsequent result(s). @@ -833,11 +819,11 @@ public boolean execute(final String sql, final int autoGeneratedKeys) throws SQL * indexes of the columns in the target table that contain the auto-generated keys that should be made available. * The driver will ignore the array if the SQL statement is not an INSERT statement, or an SQL * statement able to return auto-generated keys (the list of such statements is vendor-specific). - * + * * Under some (uncommon) situations, a single SQL statement may return multiple result sets and/or update counts. * Normally you can ignore this unless you are (1) executing a stored procedure that you know may return multiple * results or (2) you are dynamically executing an unknown SQL string. - * + * * The execute method executes an SQL statement and indicates the form of the first result. You must * then use the methods getResultSet or getUpdateCount to retrieve the result, and * getMoreResults to move to any subsequent result(s). @@ -867,11 +853,11 @@ public boolean execute(final String sql, final int[] columnIndexes) throws SQLEx * names of the columns in the target table that contain the auto-generated keys that should be made available. The * driver will ignore the array if the SQL statement is not an INSERT statement, or an SQL statement * able to return auto-generated keys (the list of such statements is vendor-specific). - * + * * In some (uncommon) situations, a single SQL statement may return multiple result sets and/or update counts. * Normally you can ignore this unless you are (1) executing a stored procedure that you know may return multiple * results or (2) you are dynamically executing an unknown SQL string. - * + * * The execute method executes an SQL statement and indicates the form of the first result. You must * then use the methods getResultSet or getUpdateCount to retrieve the result, and * getMoreResults to move to any subsequent result(s). @@ -925,18 +911,18 @@ public boolean isClosed() throws SQLException { * Requests that a Statement be pooled or not pooled. The value specified is a hint to the statement * pool implementation indicating whether the applicaiton wants the statement to be pooled. It is up to the * statement pool manager as to whether the hint is used. - * + * * The poolable value of a statement is applicable to both internal statement caches implemented by the driver and * external statement caches implemented by application servers and other applications. - * + * * By default, a Statement is not poolable when created, and a PreparedStatement and * CallableStatement are poolable when created. - * + * * * @param poolable requests that the statement be pooled if true and that the statement not be pooled if false - * + * * @throws java.sql.SQLException if this method is called on a closed Statement - * + * * @since 1.6 */ public void setPoolable(final boolean poolable) throws SQLException { @@ -945,15 +931,15 @@ public void setPoolable(final boolean poolable) throws SQLException { /** * Returns a value indicating whether the Statement is poolable or not. - * + * * * @return true if the Statement is poolable; false otherwise - * + * * @throws java.sql.SQLException if this method is called on a closed Statement - * + * * @see java.sql.Statement#setPoolable(boolean) setPoolable(boolean) * @since 1.6 - * + * */ public boolean isPoolable() throws SQLException { return false; @@ -976,21 +962,22 @@ public int getUpdateCount() throws SQLException { private boolean getMoreResults(boolean streaming) throws SQLException { + connection.lock.writeLock().lock(); try { - synchronized(protocol) { - if (queryResult != null) { - queryResult.close(); - } - - queryResult = protocol.getMoreResults(streaming); - if(queryResult == null) return false; - warningsCleared = false; - connection.reenableWarnings(); - return true; + if (queryResult != null) { + queryResult.close(); } + + queryResult = protocol.getMoreResults(streaming); + if(queryResult == null) return false; + warningsCleared = false; + connection.reenableWarnings(); + return true; } catch (QueryException e) { SQLExceptionMapper.throwException(e, connection, this); return false; + } finally { + connection.lock.writeLock().unlock(); } } @@ -998,10 +985,10 @@ private boolean getMoreResults(boolean streaming) throws SQLException { * Moves to this Statement object's next result, returns true if it is a * ResultSet object, and implicitly closes any current ResultSet object(s) obtained with * the method getResultSet. - * + * * There are no more results when the following is true:
 // stmt is a Statement object
      * ((stmt.getMoreResults() == false) && (stmt.getUpdateCount() == -1)) 
- * + * * @return true if the next result is a ResultSet object; false if it is an * update count or there are no more results * @throws java.sql.SQLException if a database access error occurs or this method is called on a closed @@ -1009,13 +996,13 @@ private boolean getMoreResults(boolean streaming) throws SQLException { * @see #execute */ public boolean getMoreResults() throws SQLException { - if (!isStreaming()) { + if (!isStreaming()) { /* return pre-cached result set, if available */ if(cachedResultSets.isEmpty()) { queryResult = null; return false; } - + Object o = cachedResultSets.remove(); if (o instanceof SQLException) throw (SQLException)o; @@ -1029,7 +1016,7 @@ public boolean getMoreResults() throws SQLException { /** * Gives the driver a hint as to the direction in which rows will be processed in ResultSet objects * created using this Statement object. The default value is ResultSet.FETCH_FORWARD. - * + * * Note that this method sets the default fetch direction for result sets generated by this Statement * object. Each result set has its own methods for getting and setting its own fetch direction. * @@ -1074,7 +1061,7 @@ public int getFetchDirection() throws SQLException { public void setFetchSize(final int rows) throws SQLException { if (rows < 0 && rows != Integer.MIN_VALUE) throw new SQLException("invalid fetch size"); - this.fetchSize = rows; + this.fetchSize = rows; } /** @@ -1123,7 +1110,7 @@ public int getResultSetType() throws SQLException { /** * Adds the given SQL command to the current list of commmands for this Statement object. The commands * in this list can be executed as a batch by calling the method executeBatch. - * + * * * @param sql typically this is a SQL INSERT or UPDATE statement * @throws java.sql.SQLException if a database access error occurs, this method is called on a closed @@ -1139,29 +1126,29 @@ public void addBatch(final String sql) throws SQLException { isInsertRewriteable(sql); batchQueries.add(new MySQLQuery(sql)); } - + /** * Parses the sql string to understand whether it is compatible with rewritten batches. * @param sql the sql string */ protected void isInsertRewriteable(String sql) { - if (!isRewriteable) { - return; - } - int index = getInsertIncipit(sql); - if (index == -1) { - isRewriteable = false; - return; - } - if (firstRewrite == null) { - firstRewrite = sql.substring(0, index); - } - boolean isRewrite = sql.startsWith(firstRewrite); + if (!isRewriteable) { + return; + } + int index = getInsertIncipit(sql); + if (index == -1) { + isRewriteable = false; + return; + } + if (firstRewrite == null) { + firstRewrite = sql.substring(0, index); + } + boolean isRewrite = sql.startsWith(firstRewrite); if (isRewrite) { - isRewriteable = isRewriteable && true; + isRewriteable = isRewriteable && true; } } - + /** * Parses the input string to understand if it is an INSERT statement. * Returns the position of the round bracket after the VALUE(S) SQL keyword, @@ -1172,36 +1159,36 @@ protected void isInsertRewriteable(String sql) { * or -1 if it cannot be parsed as an INSERT statement */ protected int getInsertIncipit(String sql) { - String sqlUpper = sql.toUpperCase(); - - if (! sqlUpper.startsWith("INSERT")) - return -1; - - int idx = sqlUpper.indexOf(" VALUE"); - int startBracket = sqlUpper.indexOf("(", idx); - int endBracket = sqlUpper.indexOf(")", startBracket); - - // Check for semicolons. Allow them inside the VALUES() brackets, otherwise return -1 - // there can be multiple, so let's loop through them - - int semicolonPos = sqlUpper.indexOf(';'); - - while (semicolonPos > -1) - { - if (semicolonPos < startBracket || semicolonPos > endBracket) - return -1; - - semicolonPos = sqlUpper.indexOf(';', semicolonPos + 1); - } - - return startBracket; + String sqlUpper = sql.toUpperCase(); + + if (! sqlUpper.startsWith("INSERT")) + return -1; + + int idx = sqlUpper.indexOf(" VALUE"); + int startBracket = sqlUpper.indexOf("(", idx); + int endBracket = sqlUpper.indexOf(")", startBracket); + + // Check for semicolons. Allow them inside the VALUES() brackets, otherwise return -1 + // there can be multiple, so let's loop through them + + int semicolonPos = sqlUpper.indexOf(';'); + + while (semicolonPos > -1) + { + if (semicolonPos < startBracket || semicolonPos > endBracket) + return -1; + + semicolonPos = sqlUpper.indexOf(';', semicolonPos + 1); + } + + return startBracket; } /** * Empties this Statement object's current list of SQL commands. - * + * * * @throws java.sql.SQLException if a database access error occurs, this method is called on a closed * Statement or the driver does not support batch updates @@ -1226,17 +1213,17 @@ public void clearBatch() throws SQLException { * count giving the number of rows in the database that were affected by the command's execution
  • A value of * SUCCESS_NO_INFO -- indicates that the command was processed successfully but that the number of rows * affected is unknown - * + * * If one of the commands in a batch update fails to execute properly, this method throws a * BatchUpdateException, and a JDBC driver may or may not continue to process the remaining commands in * the batch. However, the driver's behavior must be consistent with a particular DBMS, either always continuing to * process commands or never continuing to process commands. If the driver continues processing after a failure, * the array returned by the method BatchUpdateException.getUpdateCounts will contain as many elements * as there are commands in the batch, and at least one of the elements will be the following: - * + * *
  • A value of EXECUTE_FAILED -- indicates that the command failed to execute successfully and * occurs only if a driver continues to process commands after a command fails - * + * * The possible implementations and return values have been modified in the Java 2 SDK, Standard Edition, version * 1.3 to accommodate the option of continuing to proccess commands in a batch update after a * BatchUpdateException obejct has been thrown. @@ -1258,39 +1245,36 @@ public int[] executeBatch() throws SQLException { int[] ret = new int[batchQueries.size()]; int i = 0; MySQLResultSet rs = null; - - boolean allowMultiQueries = "true".equals(getProtocol().getInfo().getProperty("allowMultiQueries")); - boolean rewriteBatchedStatements = "true".equals(getProtocol().getInfo().getProperty("rewriteBatchedStatements")); - if (rewriteBatchedStatements) allowMultiQueries=true; + connection.lock.writeLock().lock(); try { - synchronized (this.protocol) { - if (allowMultiQueries) { - int size = batchQueries.size(); - boolean rewrittenBatch = isRewriteable && rewriteBatchedStatements; - MySQLStatement ps = (MySQLStatement) connection.createStatement(); - ps.execute(batchQueries, rewrittenBatch, rewrittenBatch?firstRewrite.length():0); - return rewrittenBatch?getUpdateCountsForReWrittenBatch(ps, size):getUpdateCounts(ps, size); - } else { - for(; i < batchQueries.size(); i++) { - execute(batchQueries.get(i)); - int updateCount = getUpdateCount(); - if (updateCount == -1) { - ret[i] = SUCCESS_NO_INFO; - } else { - ret[i] = updateCount; - } - if (i == 0) { - rs = (MySQLResultSet)getGeneratedKeys(); - } else { - rs = rs.joinResultSets((MySQLResultSet)getGeneratedKeys()); - } - } - } - } + if (getProtocol().getOptions().allowMultiQueries || getProtocol().getOptions().rewriteBatchedStatements) { + int size = batchQueries.size(); + boolean rewrittenBatch = isRewriteable && getProtocol().getOptions().rewriteBatchedStatements; + MySQLStatement ps = (MySQLStatement) connection.createStatement(); + ps.execute(batchQueries, rewrittenBatch, rewrittenBatch ? firstRewrite.length() : 0); + return rewrittenBatch?getUpdateCountsForReWrittenBatch(ps, size):getUpdateCounts(ps, size); + } else { + for(; i < batchQueries.size(); i++) { + execute(batchQueries.get(i)); + int updateCount = getUpdateCount(); + if (updateCount == -1) { + ret[i] = SUCCESS_NO_INFO; + } else { + ret[i] = updateCount; + } + if (i == 0) { + rs = (MySQLResultSet)getGeneratedKeys(); + } else { + rs = rs.joinResultSets((MySQLResultSet)getGeneratedKeys()); + } + } + } + } catch (SQLException sqle) { - throw new BatchUpdateException(sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), Arrays.copyOf(ret, i), sqle); + throw new BatchUpdateException(sqle.getMessage(), sqle.getSQLState(), sqle.getErrorCode(), Arrays.copyOf(ret, i), sqle); } finally { - clearBatch(); + connection.lock.writeLock().unlock(); + clearBatch(); } batchResultSet = rs; return ret; @@ -1302,8 +1286,8 @@ public int[] executeBatch() throws SQLException { * @param statement the rewritten statement * @return an array of update counts containing one element for each command in the batch. * The elements of the array are ordered according to the order in which commands were added to the batch. - * @param size - * @throws SQLException + * @param size the number of batch statement + * @throws SQLException if the connection has interruption */ protected int[] getUpdateCounts(Statement statement, int size) throws SQLException { int[] result = new int[size]; @@ -1332,7 +1316,7 @@ protected int[] getUpdateCountsForReWrittenBatch(Statement statement, int size) /** * Returns an object that implements the given interface to allow access to non-standard methods, or standard * methods not exposed by the proxy. - * + * * If the receiver implements the interface then the result is the receiver or a proxy for the receiver. If the * receiver is a wrapper and the wrapped object implements the interface then the result is the wrapped object or a * proxy for the wrapped object. Otherwise return the the result of calling unwrap recursively on the @@ -1345,17 +1329,17 @@ protected int[] getUpdateCountsForReWrittenBatch(Statement statement, int size) * @since 1.6 */ @SuppressWarnings("unchecked") - public T unwrap(final Class iface) throws SQLException { - try { - if (isWrapperFor(iface)) { - return (T)this; - } else { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } catch (Exception e) { - throw new SQLException("The receiver is not a wrapper and does not implement the interface"); - } - } + public T unwrap(final Class iface) throws SQLException { + try { + if (isWrapperFor(iface)) { + return (T)this; + } else { + throw new SQLException("The receiver is not a wrapper and does not implement the interface"); + } + } catch (Exception e) { + throw new SQLException("The receiver is not a wrapper and does not implement the interface"); + } + } /** * Returns true if this either implements the interface argument or is directly or indirectly a wrapper for an @@ -1376,37 +1360,29 @@ public boolean isWrapperFor(final Class interfaceOrWrapper) throws SQLExcepti return interfaceOrWrapper.isInstance(this); } + public void closeOnCompletion() throws SQLException { + // TODO Auto-generated method stub - /** - * returns the query result. - * - * @return the queryresult - */ - protected QueryResult getQueryResult() { - return queryResult; } - /** - * sets the current query result - * - * @param result - */ - protected void setQueryResult(final QueryResult result) { - this.queryResult = result; + public boolean isCloseOnCompletion() throws SQLException { + // TODO Auto-generated method stub + return false; } - public void closeOnCompletion() throws SQLException { - // TODO Auto-generated method stub - - } - - public boolean isCloseOnCompletion() throws SQLException { - // TODO Auto-generated method stub - return false; - } - public static void unloadDriver() { if (timer != null) timer.cancel(); } + + // Part of query prolog - check if connection is broken and reconnect + private void checkReconnectWithoutProxy() throws SQLException { + if (protocol.shouldReconnectWithoutProxy()) { + try { + protocol.connectWithoutProxy(); + } catch (QueryException qe) { + SQLExceptionMapper.throwException(qe, connection, this); + } + } + } } diff --git a/src/main/java/org/mariadb/jdbc/MySQLXAResource.java b/src/main/java/org/mariadb/jdbc/MySQLXAResource.java index 899d56eec..77641eec6 100644 --- a/src/main/java/org/mariadb/jdbc/MySQLXAResource.java +++ b/src/main/java/org/mariadb/jdbc/MySQLXAResource.java @@ -145,7 +145,7 @@ public boolean setTransactionTimeout(int timeout) throws XAException { public void start(Xid xid, int flags) throws XAException { if (flags != TMJOIN && flags != TMRESUME && flags != TMNOFLAGS) throw new XAException(XAException.XAER_INVAL); - if (flags == TMJOIN && "true".equalsIgnoreCase(connection.getPinGlobalTxToPhysicalConnection())) { + if (flags == TMJOIN && connection.getPinGlobalTxToPhysicalConnection()) { flags = TMRESUME; } execute("XA START " + xidToString(xid) + " "+flagsToString(flags)); diff --git a/src/main/java/org/mariadb/jdbc/Version.java b/src/main/java/org/mariadb/jdbc/Version.java index ee6596c97..1b4106f1e 100644 --- a/src/main/java/org/mariadb/jdbc/Version.java +++ b/src/main/java/org/mariadb/jdbc/Version.java @@ -1,7 +1,10 @@ package org.mariadb.jdbc; public final class Version { - public static final String build_time="20150706-1223"; - public static final String pomversion="1.1.10-SNAPSHOT"; + public static final String version = "1.2.0-SNAPSHOT"; + public static final int majorVersion = 1; + public static final int minorVersion = 2; + public static final int patchVersion = 0; + public static final String qualifier = "SNAPSHOT"; } \ No newline at end of file diff --git a/src/main/java/org/mariadb/jdbc/internal/common/DefaultOptions.java b/src/main/java/org/mariadb/jdbc/internal/common/DefaultOptions.java new file mode 100644 index 000000000..739318692 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/common/DefaultOptions.java @@ -0,0 +1,448 @@ +/* +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.common; + +import java.lang.invoke.WrongMethodTypeException; +import java.util.Properties; + +public enum DefaultOptions { + /** + * Database user name + */ + USER("user", "1.0.0"), + /** + * Password of database user + */ + PASSWORD("password", "1.0.0"), + + CONNECT_TIMOUT("connectTimeout", (Integer) null, new Integer(0), Integer.MAX_VALUE, "1.1.8"), + + /** + * On Windows, specify named pipe name to connect to mysqld.exe + */ + PIPE("pipe", "1.1.3"), + + /** + * Allows to connect to database via Unix domain socket, if server allows it. The value is the path of Unix domain socket, i.e "socket" database parameter + */ + LOCAL_SOCKET("localSocket", "1.1.4"), + + /** + * Allowed to connect database via shared memory, if server allows it. The value is base name of the shared memory + */ + SHARED_MEMORY("sharedMemory", "1.1.4"), + + /** + * Sets corresponding option on the connection socket + */ + TCP_NO_DELAY("tcpNoDelay", Boolean.FALSE, "1.0.0"), + + /** + * Sets corresponding option on the connection socket + */ + TCP_ABORTIVE_CLOSE("tcpAbortiveClose", Boolean.FALSE, "1.1.1"), + + /** + * Hostname or IP address to bind the connection socket to a local (UNIX domain) socket. + */ + LOCAL_SOCKET_ADDRESS("localSocketAddress", "1.1.8"), + + /** + * Defined the network socket timeout (SO_TIMEOUT) in milliseconds. + * 0 (default) disable this timeout + */ + SOCKET_TIMEOUT("socketTimeout", new Integer[]{10000, null, null, null}, new Integer(0), Integer.MAX_VALUE, "1.1.8"), + + /** + * Session timeout is defined by the wait_timeout server variable. + * Setting interactiveClient to true will tell server to use the interactive_timeout server variable + */ + INTERACTIVE_CLIENT("interactiveClient", Boolean.FALSE, "1.1.8"), + + /** + * If set to 'true', exception thrown during query execution contain query string + */ + DUMP_QUERY_ON_EXCEPTION("dumpQueriesOnException", Boolean.FALSE, "1.1.0"), + + /** + * Metadata ResultSetMetaData.getTableName() return the physical table name. + * "useOldAliasMetadataBehavior" permit to activate the legacy code that send the table alias if set. + */ + USE_OLD_ALIAS_METADATA_BEHAVIOR("useOldAliasMetadataBehavior", Boolean.FALSE, "1.1.9"), + + /** + * var=value pairs separated by comma, mysql session variables, set upon establishing successful connection + */ + SESSION_VARIABLES("sessionVariables", "1.1.0"), + + /** + * The database precised in url will be created if doesn't exist + */ + CREATE_DATABASE_IF_NOT_EXISTS("createDatabaseIfNotExist", Boolean.FALSE, "1.1.8"), + + /** + * Defined the server time zone. + * to use only if jre server as a different time implementation of the server. + * (best to have the same server time zone when possible) + */ + SERVER_TIMEZONE("serverTimezone", "1.1.8"), + /** + * DatabaseMetaData use current catalog if null + */ + NULL_CATALOG_MEANS_CURRENT("nullCatalogMeansCurrent", Boolean.TRUE, "1.1.8"), + + /** + * Datatype mapping flag, handle MySQL Tiny as BIT(boolean) + */ + TINY_INT_IS_BIT("tinyInt1isBit", Boolean.TRUE, "1.0.0"), + + /** + * Year is date type, rather than numerical + */ + YEAR_IS_DATE_TYPE("yearIsDateType", Boolean.TRUE, "1.0.0"), + + /** + * Force SSL on connection + */ + USE_SSL("useSSL", Boolean.FALSE, "1.1.0"), + + /** + * allow compression in MySQL Protocol + */ + USER_COMPRESSION("useCompression", Boolean.FALSE, "1.0.0"), + + /** + * Allows multiple statements in single executeQuery + */ + ALLOW_MULTI_QUERIES("allowMultiQueries", Boolean.FALSE, "1.0.0"), + + /** + * rewrite batchedStatement to have only one server call + */ + REWRITE_BATCHED_STATEMENTS("rewriteBatchedStatements", Boolean.FALSE, "1.1.8"), + + /** + * Sets corresponding option on the connection socket + */ + TCP_KEEP_ALIVE("tcpKeepAlive", Boolean.FALSE, "1.0.0"), + + /** + * set buffer size for TCP buffer (SO_RCVBUF) + */ + TCP_RCV_BUF("tcpRcvBuf", (Integer) null, new Integer(0), Integer.MAX_VALUE, "1.0.0"), + + /** + * set buffer size for TCP buffer (SO_SNDBUF) + */ + TCP_SND_BUF("tcpSndBuf", (Integer) null, new Integer(0), Integer.MAX_VALUE, "1.0.0"), + + /** + * to use custom socket factory, set it to full name of the class that implements javax.net.SocketFactory + */ + SOCKET_FACTORY("socketFactory", "1.0.0"), + PIN_GLOBAL_TX_TO_PHYSICAL_CONNECTION("pinGlobalTxToPhysicalConnection", Boolean.FALSE, "1.1.8"), + + /** + * When using SSL, do not check server's certificate + */ + TRUST_SERVER_CERTIFICATE("trustServerCertificate", Boolean.FALSE, "1.1.1"), + + /** + * Server's certificatem in DER form, or server's CA certificate. Can be used in one of 3 forms, sslServerCert=/path/to/cert.pem (full path to certificate), sslServerCert=classpath:relative/cert.pem (relative to current classpath), or as verbatim DER-encoded certificate string "------BEGING CERTIFICATE-----" + */ + SERVER_SSL_CERT("serverSslCert", "1.1.3"), + + /** + * Correctly handle subsecond precision in timestamps (feature available with MariaDB 5.3 and later).May confuse 3rd party components (Hibernated) + */ + USE_FRACTIONAL_SECONDS("useFractionalSeconds", Boolean.TRUE, "1.0.0"), + + /** + * Driver must recreateConnection after a failover + */ + AUTO_RECONNECT("autoReconnect", Boolean.FALSE, "1.2.0"), + + /** + * After a master failover and no other master found, back on a read-only host ( throw exception if not) + */ + FAIL_ON_READ_ONLY("failOnReadOnly", Boolean.FALSE, "1.2.0"), + + /** + * If autoReconnect is enabled, the initial time to wait between re-connect attempts (in seconds, defaults to 2) + */ + INITIAL_TIMEOUT("initialTimeout", new Integer(2), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + /** + * 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 + */ + SECONDS_BEFORE_RETRY_MASTER("secondsBeforeRetryMaster", new Integer(50), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + /** + * 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 + */ + QUERY_BEFORE_RETRY_MASTER("queriesBeforeRetryMaster", new Integer(30), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + /** + * 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. + */ + RETRY_ALL_DOWN("retriesAllDown", new Integer(120), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + /** + * When using failover, the number of times the driver should cycle silently through available hosts, attempting to connect. + * Between cycles, the driver will pause for 250ms if no servers are available. + * if set to 0, there will be no silent reconnection + */ + FAILOVER_LOOP_RETRIES("failoverLoopRetries", new Integer(120), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + + /** + * When in multiple hosts, after this time in second without used, verification that the connections havn't been lost. + * When 0, no verification will be done. Defaults to 120 + */ + VALID_CONNECTION_TIMEOUT("validConnectionTimeout", new Integer(120), new Integer(0), Integer.MAX_VALUE, "1.2.0"), + + /** + * time in second a server is blacklisted after a connection failure. default to 50s + */ + LOAD_BALANCE_BLACKLIST_TIMEOUT("loadBalanceBlacklistTimeout", new Integer(50), new Integer(0), Integer.MAX_VALUE, "1.2.0"); + + protected final String name; + protected final Object objType; + protected final Object defaultValue; + protected final Object minValue; + protected final Object maxValue; + protected final String implementationVersion; + protected Object value = null; + + DefaultOptions(String name, String implementationVersion) { + this.name = name; + this.implementationVersion = implementationVersion; + objType = String.class; + defaultValue = null; + minValue = null; + maxValue = null; + } + + DefaultOptions(String name, Boolean defaultValue, String implementationVersion) { + this.name = name; + this.objType = Boolean.class; + this.defaultValue = defaultValue; + this.implementationVersion = implementationVersion; + minValue = null; + maxValue = null; + } + + DefaultOptions(String name, Integer defaultValue, Integer minValue, Integer maxValue, String implementationVersion) { + this.name = name; + this.objType = Integer.class; + this.defaultValue = defaultValue; + this.minValue = minValue; + this.maxValue = maxValue; + this.implementationVersion = implementationVersion; + } + + + DefaultOptions(String name, Integer[] defaultValue, Integer minValue, Integer maxValue, String implementationVersion) { + this.name = name; + this.objType = Integer.class; + this.defaultValue = defaultValue; + this.minValue = minValue; + this.maxValue = maxValue; + this.implementationVersion = implementationVersion; + } + + private void setIntValue(Integer value) { + this.value = value; + } + + private void setBooleanValue(Boolean value) { + this.value = value; + } + + public int intValue() { + if (objType.equals(Integer.class)) { + if (value != null) return ((Integer) value).intValue(); + else return ((Integer) defaultValue).intValue(); + } else + throw new WrongMethodTypeException("Method " + name + " is of type " + objType + " intValue() does not apply"); + } + + public boolean boolValue() { + if (objType.equals(Boolean.class)) { + if (value != null) return ((Boolean) value).booleanValue(); + else return ((Boolean) defaultValue).booleanValue(); + } else + throw new WrongMethodTypeException("Method " + name + " is of type " + objType + " intValue() does not apply"); + } + public String stringValue() { + if (objType.equals(String.class)) { + return ((String) value); + } else + throw new WrongMethodTypeException("Method " + name + " is of type " + objType + " intValue() does not apply"); + } + + public static Options defaultValues(UrlHAMode haMode) { + return parse(haMode, "", new Properties()); + } + + public static Options parse(UrlHAMode haMode, String urlParameters, Options options) { + Properties prop = new Properties(); + return parse(haMode, urlParameters, prop, options); + } + + public static Options parse(UrlHAMode haMode, String urlParameters, Properties properties) { + return parse(haMode, urlParameters, properties, null); + } + + public static Options addProperty(UrlHAMode haMode, String name, String value, Options options) { + Properties additionnalProperties = new Properties(); + additionnalProperties.put(name, value); + return parse(haMode, additionnalProperties, options); + } + + public static Options addProperty(UrlHAMode haMode, Properties additionnalProperties, Options options) { + return parse(haMode, additionnalProperties, options); + } + + private static Options parse(UrlHAMode haMode, String urlParameters, Properties properties, Options options) { + if (!"".equals(urlParameters)) { + String[] parameters = urlParameters.split("&"); + for (String parameter : parameters) { + int pos = parameter.indexOf('='); + if (pos == -1) { + throw new IllegalArgumentException("Invalid connection URL, expected key=value pairs, found " + parameter); + } + properties.setProperty(parameter.substring(0, pos), parameter.substring(pos + 1)); + } + } + return parse(haMode, properties, options); + } + + private static Options parse(UrlHAMode haMode, Properties properties, Options options) { + boolean initial = false; + if (options==null) { + options = new Options(); + initial = true; + } + + try { + for (DefaultOptions o : DefaultOptions.values()) { + + String propertieValue = properties.getProperty(o.name); + if (propertieValue == null && o.name.equals("createDatabaseIfNotExist")) + propertieValue = properties.getProperty("createDB"); + + if (propertieValue != null) { + if (o.objType.equals(String.class)) { + Options.class.getField(o.name).set(options, propertieValue); + } else if (o.objType.equals(Boolean.class)) { + String value = propertieValue.toLowerCase(); + if ("1".equals(value)) value = "true"; + else if ("0".equals(value)) value = "false"; + if (!"true".equals(value) && !"false".equals(value)) + throw new IllegalArgumentException("Optional parameter " + o.name + " must be boolean (true/false or 0/1) was \"" + propertieValue + "\""); + Options.class.getField(o.name).set(options, Boolean.valueOf(value)); + } else if (o.objType.equals(Integer.class)) { + try { + Integer value = Integer.parseInt(propertieValue); + if (value.compareTo((Integer) o.minValue) < 0 || value.compareTo((Integer) o.maxValue) > 0) + throw new IllegalArgumentException("Optional parameter " + o.name + " must be greater or equal to " + o.minValue + ((((Integer) o.maxValue).intValue() != Integer.MAX_VALUE) ? " and smaller than " + o.maxValue : " ") + ", was \"" + propertieValue + "\""); + Options.class.getField(o.name).set(options, value); + } catch (NumberFormatException n) { + throw new IllegalArgumentException("Optional parameter " + o.name + " must be Integer, was \"" + propertieValue + "\""); + } + } + } else { + if (initial) { + if (o.defaultValue instanceof Integer[]) { + Options.class.getField(o.name).set(options, ((Integer[])o.defaultValue)[haMode.ordinal()]); + } else Options.class.getField(o.name).set(options, o.defaultValue); + } + } + } + } catch (NoSuchFieldException | IllegalAccessException n) { + n.printStackTrace(); + } catch (SecurityException s) { + //only for jws, so never thrown + throw new IllegalArgumentException("Security too restrictive : " + s.getMessage()); + } + return options; + } + + public static Properties getProperties(Options options) { + Properties prop = new Properties(); + try { + for (DefaultOptions o : DefaultOptions.values()) { + Object obj = Options.class.getField(o.name).get(options); + if (obj != null) { + prop.put(o.name, String.valueOf(Options.class.getField(o.name).get(options)).toString()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return prop; + } + + + public static String getProperties(String optionName, Options options) { + try { + return String.valueOf(Options.class.getField(optionName).get(options)); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/mariadb/jdbc/internal/common/Options.java b/src/main/java/org/mariadb/jdbc/internal/common/Options.java new file mode 100644 index 000000000..f49a7ce52 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/common/Options.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.common; + +public class Options { + //standard options + public String user; + public String password; + + //divers + public boolean trustServerCertificate; + public String serverSslCert; + public boolean useFractionalSeconds; + public boolean pinGlobalTxToPhysicalConnection; + public String socketFactory; + public Integer connectTimeout; + public String pipe; + public String localSocket; + public String sharedMemory; + public boolean tcpNoDelay; + public boolean tcpKeepAlive; + public Integer tcpRcvBuf; + public Integer tcpSndBuf; + public boolean tcpAbortiveClose; + public String localSocketAddress; + public Integer socketTimeout; + public boolean allowMultiQueries; + public boolean rewriteBatchedStatements; + public boolean useCompression; + public boolean interactiveClient; + public boolean useSSL; + public String sessionVariables; + public boolean tinyInt1isBit; + public boolean yearIsDateType; + public boolean createDatabaseIfNotExist; + public String serverTimezone; + public boolean nullCatalogMeansCurrent; + public boolean dumpQueriesOnException; + public boolean useOldAliasMetadataBehavior; + + //HA options + public boolean autoReconnect; + public boolean failOnReadOnly; + public int initialTimeout; + public int secondsBeforeRetryMaster; + public int queriesBeforeRetryMaster; + public int retriesAllDown; + public int validConnectionTimeout; + public int loadBalanceBlacklistTimeout; + public int failoverLoopRetries; + + @Override + public String toString() { + return "Options{" + + "user='" + user + '\'' + + ", password='" + password + '\'' + + ", trustServerCertificate=" + trustServerCertificate + + ", serverSslCert='" + serverSslCert + '\'' + + ", useFractionalSeconds=" + useFractionalSeconds + + ", pinGlobalTxToPhysicalConnection=" + pinGlobalTxToPhysicalConnection + + ", socketFactory='" + socketFactory + '\'' + + ", connectTimeout=" + connectTimeout + + ", pipe='" + pipe + '\'' + + ", localSocket='" + localSocket + '\'' + + ", sharedMemory='" + sharedMemory + '\'' + + ", tcpNoDelay=" + tcpNoDelay + + ", tcpKeepAlive=" + tcpKeepAlive + + ", tcpRcvBuf=" + tcpRcvBuf + + ", tcpSndBuf=" + tcpSndBuf + + ", tcpAbortiveClose=" + tcpAbortiveClose + + ", localSocketAddress='" + localSocketAddress + '\'' + + ", socketTimeout=" + socketTimeout + + ", allowMultiQueries=" + allowMultiQueries + + ", rewriteBatchedStatements=" + rewriteBatchedStatements + + ", useCompression=" + useCompression + + ", interactiveClient=" + interactiveClient + + ", useSSL=" + useSSL + + ", sessionVariables='" + sessionVariables + '\'' + + ", tinyInt1isBit=" + tinyInt1isBit + + ", yearIsDateType=" + yearIsDateType + + ", createDatabaseIfNotExist=" + createDatabaseIfNotExist + + ", serverTimezone='" + serverTimezone + '\'' + + ", nullCatalogMeansCurrent=" + nullCatalogMeansCurrent + + ", dumpQueriesOnException=" + dumpQueriesOnException + + ", useOldAliasMetadataBehavior=" + useOldAliasMetadataBehavior + + ", autoReconnect=" + autoReconnect + + ", failOnReadOnly=" + failOnReadOnly + + ", initialTimeout=" + initialTimeout + + ", secondsBeforeRetryMaster=" + secondsBeforeRetryMaster + + ", queriesBeforeRetryMaster=" + queriesBeforeRetryMaster + + ", retriesAllDown=" + retriesAllDown + + ", validConnectionTimeout=" + validConnectionTimeout + + ", loadBalanceBlacklistTimeout=" + loadBalanceBlacklistTimeout + + ", failoverLoopRetries=" + failoverLoopRetries + + '}'; + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/common/ParameterConstant.java b/src/main/java/org/mariadb/jdbc/internal/common/ParameterConstant.java new file mode 100644 index 000000000..94fb433f8 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/common/ParameterConstant.java @@ -0,0 +1,53 @@ +package org.mariadb.jdbc.internal.common; +/* +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. +*/ +public class ParameterConstant { + public final static String TYPE_MASTER = "master"; + public final static String TYPE_SLAVE = "slave"; +} diff --git a/src/main/java/org/mariadb/jdbc/internal/common/UrlHAMode.java b/src/main/java/org/mariadb/jdbc/internal/common/UrlHAMode.java new file mode 100644 index 000000000..70d63aed9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/common/UrlHAMode.java @@ -0,0 +1,53 @@ +/* +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.common; + +public enum UrlHAMode { + AURORA, REPLICATION, FAILOVER, NONE +} diff --git a/src/main/java/org/mariadb/jdbc/internal/common/Utils.java b/src/main/java/org/mariadb/jdbc/internal/common/Utils.java index d3ccf8bdf..747a4dafb 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/Utils.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/Utils.java @@ -48,12 +48,19 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS */ package org.mariadb.jdbc.internal.common; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.mysql.*; +import org.mariadb.jdbc.internal.mysql.listener.impl.AuroraListener; +import org.mariadb.jdbc.internal.mysql.listener.impl.MastersFailoverListener; +import org.mariadb.jdbc.internal.mysql.listener.impl.MastersSlavesListener; + +import java.lang.reflect.Proxy; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; -import java.util.Properties; +import java.util.concurrent.locks.ReentrantReadWriteLock; public class Utils { @@ -511,30 +518,28 @@ else if (lastChar == '*') { return sqlBuffer.toString(); } - /** - * Adds the parsed parameter to the properties object. - * - * @param parameter a key=value pair - * @param info the properties object - */ - public static void setUrlParameter(String parameter, Properties info) { - int pos = parameter.indexOf('='); - if (pos == -1) { - throw new IllegalArgumentException("Invalid connection URL, expected key=value pairs, found " + parameter); + + + public static Protocol retrieveProxy(final JDBCUrl jdbcUrl, final ReentrantReadWriteLock lock) throws QueryException, SQLException { + if (jdbcUrl.getHaMode().equals(UrlHAMode.AURORA)) { + return (Protocol) Proxy.newProxyInstance( + AuroraProtocol.class.getClassLoader(), + new Class[] {Protocol.class}, + new FailoverProxy(new AuroraListener(jdbcUrl), lock)); + } else if (jdbcUrl.getHaMode().equals(UrlHAMode.REPLICATION)){ + return (Protocol) Proxy.newProxyInstance( + MastersSlavesProtocol.class.getClassLoader(), + new Class[] {Protocol.class}, + new FailoverProxy(new MastersSlavesListener(jdbcUrl), lock)); + } else if (jdbcUrl.getHaMode().equals(UrlHAMode.FAILOVER)){ + return (Protocol) Proxy.newProxyInstance( + MySQLProtocol.class.getClassLoader(), + new Class[]{Protocol.class}, + new FailoverProxy(new MastersFailoverListener(jdbcUrl), lock)); + } else { + MySQLProtocol protocol = new MySQLProtocol(jdbcUrl, lock); + protocol.connectWithoutProxy(); + return protocol; } - info.setProperty(parameter.substring(0, pos), parameter.substring(pos + 1)); - } - - /** - * Parses the parameters string and sets the corresponding properties in the properties object. - * - * @param urlParameters the parameters string - * @param info the properties object - */ - public static void setUrlParameters(String urlParameters, Properties info) { - String [] parameters = urlParameters.split("&"); - for(String parameter : parameters) { - setUrlParameter(parameter, info); - } } } diff --git a/src/main/java/org/mariadb/jdbc/internal/common/packet/MaxAllowedPacketException.java b/src/main/java/org/mariadb/jdbc/internal/common/packet/MaxAllowedPacketException.java index 2d0f59929..852fcd1d7 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/packet/MaxAllowedPacketException.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/packet/MaxAllowedPacketException.java @@ -1,10 +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.common.packet; import java.io.IOException; -/** - * Created by diego_000 on 03/06/2015. - */ public class MaxAllowedPacketException extends IOException { boolean mustReconnect; public MaxAllowedPacketException(String message, boolean mustReconnect) { diff --git a/src/main/java/org/mariadb/jdbc/internal/common/query/parameters/DateParameter.java b/src/main/java/org/mariadb/jdbc/internal/common/query/parameters/DateParameter.java index cd564db55..12cc9bb27 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/query/parameters/DateParameter.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/query/parameters/DateParameter.java @@ -62,7 +62,7 @@ public class DateParameter extends ParameterHolder { /** * Represents a timestamp, constructed with time in millis since epoch * - * @param date + * @param date the date */ public DateParameter(Date date) { this(date, null); diff --git a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/CachedSelectResult.java b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/CachedSelectResult.java index d5f98354f..7d60c7b6d 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/CachedSelectResult.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/CachedSelectResult.java @@ -104,7 +104,7 @@ public MySQLColumnInformation[] getColumnInformation() { * gets the value at position i in the result set. i starts at zero! * * @param i index, starts at 0 - * @return + * @return the value */ public ValueObject getValueObject(int i) throws NoSuchColumnException { if (rowPointer < 0) { diff --git a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/SelectQueryResult.java b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/SelectQueryResult.java index d5d9a27f9..9ebbd4a28 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/SelectQueryResult.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/SelectQueryResult.java @@ -79,10 +79,10 @@ public ResultSetType getResultSetType() { return ResultSetType.SELECT; } - /** + /** * moves the row pointer to position i - * - * @param i the position + * @param i pointer to move + * @throws SQLException sql feature not supported */ public void moveRowPointerTo(int i) throws SQLException{ throw new SQLFeatureNotSupportedException("scrolling result set not supported"); @@ -92,6 +92,7 @@ public void moveRowPointerTo(int i) throws SQLException{ * gets the current row number * * @return the current row number + * @throws SQLException sql feature not supported */ public int getRowPointer() throws SQLException{ throw new SQLFeatureNotSupportedException("scrolling result set not supported"); diff --git a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/StreamingSelectResult.java b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/StreamingSelectResult.java index acb2df350..27529f6db 100644 --- a/src/main/java/org/mariadb/jdbc/internal/common/queryresults/StreamingSelectResult.java +++ b/src/main/java/org/mariadb/jdbc/internal/common/queryresults/StreamingSelectResult.java @@ -26,12 +26,15 @@ private StreamingSelectResult(MySQLColumnInformation[] info, MySQLProtocol proto protocol.activeResult = this; } - /** - * create StreamingResultSet - precondition is that a result set packet has been read + + /** * * @param packet the result set packet from the server + * @param packetFetcher packetfetcher + * @param protocol the current connection protocol class * @return a StreamingQueryResult - * @throws java.io.IOException when something goes wrong while reading/writing from the server + * @throws IOException when something goes wrong while reading/writing from the server + * @throws QueryException if there is an actual active result on the current connection */ public static StreamingSelectResult createStreamingSelectResult( ResultSetPacket packet, PacketFetcher packetFetcher, MySQLProtocol protocol) @@ -142,7 +145,7 @@ public void close() { * gets the value at position i in the result set. i starts at zero! * * @param i index, starts at 0 - * @return + * @return the value */ @Override public ValueObject getValueObject(int i) throws NoSuchColumnException { diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraProtocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraProtocol.java new file mode 100644 index 000000000..d8e6c2ebf --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/AuroraProtocol.java @@ -0,0 +1,257 @@ +/* +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.query.MySQLQuery; +import org.mariadb.jdbc.internal.common.queryresults.SelectQueryResult; +import org.mariadb.jdbc.internal.mysql.listener.impl.AuroraListener; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AuroraProtocol extends MastersSlavesProtocol { + private final static Logger log = Logger.getLogger(AuroraProtocol.class.getName()); + + public AuroraProtocol(final JDBCUrl url, final ReentrantReadWriteLock lock) { + super(url, lock); + } + + + @Override + public boolean isMasterConnection() { + return this.masterConnection; + } + + /** + * Aurora best way to check if a node is a master : is not in read-only mode + * + * @return indicate if master has been found + */ + @Override + public boolean checkIfMaster() throws QueryException { + proxy.lock.writeLock().lock(); + try { + SelectQueryResult queryResult = (SelectQueryResult) executeQuery(new MySQLQuery("show global variables like 'innodb_read_only'")); + if (queryResult != null) { + queryResult.next(); + this.masterConnection = "OFF".equals(queryResult.getValueObject(1).getString()); + } else { + this.masterConnection = false; + } + this.readOnly = !this.masterConnection; + return this.masterConnection; + + } catch (IOException ioe) { + log.log(Level.FINEST, "exception during checking if master", ioe); + throw new QueryException("could not check the 'innodb_read_only' variable status on " + this.getHostAddress() + + " : " + ioe.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), ioe); + } finally { + proxy.lock.writeLock().unlock(); + } + } + + + public static void searchProbableMaster(AuroraListener listener, HostAddress probableMaster, Map blacklist, SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINE)) { + log.fine("searching for master:" + searchFilter.isSearchForMaster() + " replica:" + searchFilter.isSearchForSlave() + " address:" + probableMaster + " blacklist:" + blacklist.keySet()); + } + AuroraProtocol protocol = getNewProtocol(listener.getProxy(), listener.getJdbcUrl()); + try { + + protocol.setHostAddress(probableMaster); + if (log.isLoggable(Level.FINE)) log.fine("trying to connect to " + protocol.getHostAddress()); + protocol.connect(); + if (log.isLoggable(Level.FINE)) log.fine("connected to " + protocol.getHostAddress()); + + if (searchFilter.isSearchForMaster() && protocol.isMasterConnection()) { + searchFilter.setSearchForMaster(false); + protocol.setMustBeMasterConnection(true); + listener.foundActiveMaster(protocol); + } else if (searchFilter.isSearchForSlave() && !protocol.isMasterConnection()) { + searchFilter.setSearchForSlave(false); + protocol.setMustBeMasterConnection(false); + listener.foundActiveSecondary(protocol); + } else { + if (log.isLoggable(Level.FINE)) + log.fine("close connection because unused : " + protocol.getHostAddress()); + protocol.close(); + protocol = getNewProtocol(listener.getProxy(), listener.getJdbcUrl()); + } + + } catch (QueryException e) { + blacklist.put(protocol.getHostAddress(), System.currentTimeMillis()); + if (log.isLoggable(Level.FINE)) + log.fine("Could not connect to " + protocol.currentHost + " searching for master : " + searchFilter.isSearchForMaster() + " for replica :" + searchFilter.isSearchForSlave() + " error:" + e.getMessage()); + } + } + + /** + * loop until found the failed connection. + * + * @param listener current listener + * @param addresses list of HostAddress to loop + * @param blacklist current blacklist + * @param searchFilter search parameter + * @throws QueryException if not found + */ + public static void loop(AuroraListener listener, final List addresses, Map blacklist, SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINE)) { + log.fine("searching for master:" + searchFilter.isSearchForMaster() + " replica:" + searchFilter.isSearchForSlave() + " addresses:" + addresses ); + } + + AuroraProtocol protocol; + List loopAddresses = new LinkedList<>(addresses); + int maxConnectionTry = listener.getRetriesAllDown(); + QueryException lastQueryException = null; + + while (!loopAddresses.isEmpty() || (!searchFilter.isUniqueLoop() && maxConnectionTry > 0)) { + protocol = getNewProtocol(listener.getProxy(), listener.getJdbcUrl()); + + if (listener.isExplicitClosed() || (!listener.isSecondaryHostFail() && !listener.isMasterHostFail())) return; + maxConnectionTry--; + + try { + protocol.setHostAddress(loopAddresses.get(0)); + loopAddresses.remove(0); + + if (log.isLoggable(Level.FINE)) log.fine("trying to connect to " + protocol.getHostAddress()); + protocol.connect(); + blacklist.remove(protocol.getHostAddress()); + if (log.isLoggable(Level.FINE)) log.fine("connected to " + (protocol.isMasterConnection()?"primary ":"replica ") + protocol.getHostAddress()); + + if (searchFilter.isSearchForMaster() && protocol.isMasterConnection()) { + log.finest("locks -0 : "+protocol.getProxy().lock.getReadHoldCount() + " "+protocol.getProxy().lock.getWriteHoldCount()); + if (foundMaster(listener, protocol, searchFilter)) return; + log.finest("locks -1: "+protocol.getProxy().lock.getReadHoldCount() + " "+protocol.getProxy().lock.getWriteHoldCount()); + } else if (searchFilter.isSearchForSlave() && !protocol.isMasterConnection()) { + log.finest("locks -2: "+protocol.getProxy().lock.getReadHoldCount() + " "+protocol.getProxy().lock.getWriteHoldCount()); + if (foundSecondary(listener, protocol, searchFilter)) return; + log.finest("locks -3: "+protocol.getProxy().lock.getReadHoldCount() + " "+protocol.getProxy().lock.getWriteHoldCount()); + + HostAddress probableMasterHost = listener.searchByStartName(protocol, listener.getJdbcUrl().getHostAddresses()); + if (probableMasterHost != null) { + loopAddresses.remove(probableMasterHost); + AuroraProtocol.searchProbableMaster(listener, probableMasterHost, blacklist, searchFilter); + log.finest("locks -4: "+protocol.getProxy().lock.getReadHoldCount() + " "+protocol.getProxy().lock.getWriteHoldCount()); + if (!searchFilter.isSearchForMaster()) return; + } + } else { + protocol.close(); + } + } catch (QueryException e) { + lastQueryException = e; + blacklist.put(protocol.getHostAddress(), System.currentTimeMillis()); + if (log.isLoggable(Level.FINE)) log.fine("Could not connect to " + protocol.getHostAddress() + " searching: " + searchFilter + " error: " + e.getMessage()); + } + + if (!searchFilter.isSearchForMaster() && !searchFilter.isSearchForSlave()) return; + + //loop is set so + if (loopAddresses.isEmpty() && !searchFilter.isUniqueLoop() && maxConnectionTry > 0) { + loopAddresses = new LinkedList<>(addresses); + listener.checkIfTypeHaveChanged(searchFilter); + } + + } + + if (searchFilter.isSearchForMaster() || searchFilter.isSearchForSlave()) { + String error = "No active connection found for replica"; + if (searchFilter.isSearchForMaster()) error = "No active connection found for master"; + if (lastQueryException != null) { + throw new QueryException(error, lastQueryException.getErrorCode(), lastQueryException.getSqlState(), lastQueryException); + } + throw new QueryException(error); + } + } + + private static boolean foundMaster(AuroraListener listener, AuroraProtocol protocol,SearchFilter searchFilter) { + protocol.setMustBeMasterConnection(true); + searchFilter.setSearchForMaster(false); + listener.foundActiveMaster(protocol); + if (!searchFilter.isSearchForSlave()) return true; + else { + if (listener.isExplicitClosed() + || searchFilter.isFineIfFoundOnlyMaster() + || !listener.isSecondaryHostFail()) return true; + } + return false; + } + + private static boolean foundSecondary(AuroraListener listener, AuroraProtocol protocol,SearchFilter searchFilter) { + searchFilter.setSearchForSlave(false); + protocol.setMustBeMasterConnection(false); + listener.foundActiveSecondary(protocol); + if (!searchFilter.isSearchForMaster()) return true; + else { + if (listener.isExplicitClosed() + || searchFilter.isFineIfFoundOnlySlave() + || !listener.isMasterHostFail()) return true; + + + } + return false; + } + + public static AuroraProtocol getNewProtocol(FailoverProxy proxy, JDBCUrl jdbcUrl) { + AuroraProtocol newProtocol = new AuroraProtocol(jdbcUrl, proxy.lock); + newProtocol.setProxy(proxy); + return newProtocol; + } + + +} 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..08a379b98 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/FailoverProxy.java @@ -0,0 +1,188 @@ +/* +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 org.mariadb.jdbc.internal.mysql.listener.Listener; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Logger; + +public class FailoverProxy implements InvocationHandler { + private final static Logger log = Logger.getLogger(FailoverProxy.class.getName()); + + public final static String METHOD_IS_EXPLICIT_CLOSED = "isExplicitClosed"; + public final static String METHOD_GET_OPTIONS = "getOptions"; + public final static String METHOD_GET_PROXY = "getProxy"; + public final static String METHOD_EXECUTE_QUERY = "executeQuery"; + public final static String METHOD_SET_READ_ONLY = "setReadonly"; + public final static String METHOD_IS_READ_ONLY = "isReadOnly"; + public final static String METHOD_CLOSED_EXPLICIT = "closeExplicit"; + public final static String METHOD_IS_CLOSED = "isClosed"; + + + public final ReentrantReadWriteLock lock; + + private Listener listener; + + public FailoverProxy(Listener listener, ReentrantReadWriteLock lock) throws QueryException, SQLException{ + this.lock = lock; + this.listener = listener; + this.listener.setProxy(this); + this.listener.initializeConnection(); + } + + /** + * proxy that catch Protocol call, to permit to catch errors and handle failover when multiple hosts + * @param proxy the current protocol + * @param method the called method on the protocol + * @param args methods parameters + * @return protocol method result + * @throws Throwable the method throwed error if not catch by failover + */ + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + switch (methodName) { + case METHOD_IS_EXPLICIT_CLOSED: + return listener.isExplicitClosed(); + case METHOD_GET_OPTIONS: + return listener.getJdbcUrl().getOptions(); + case METHOD_GET_PROXY: + return this; + case METHOD_IS_CLOSED: + return listener.isClosed(); + case METHOD_EXECUTE_QUERY: + try { + this.listener.preExecute(); + } catch (QueryException e) { + return handleFailOver(e, method, args); + } + break; + case METHOD_SET_READ_ONLY: + this.listener.switchReadOnlyConnection((Boolean) args[0]); + return null; + case METHOD_IS_READ_ONLY: + return this.listener.isReadOnly(); + case METHOD_CLOSED_EXPLICIT: + this.listener.preClose(); + return null; + } + try { + return listener.invoke(method, args); + } catch (InvocationTargetException e) { + if (e.getTargetException() != null) { + if (e.getTargetException() instanceof QueryException) { + if (hasToHandleFailover((QueryException) e.getTargetException())) { + return handleFailOver((QueryException) e.getTargetException(), method, args); + } + } + throw e.getTargetException(); + } + throw e; + } + } + + + /** + * After a connection exception, launch failover + * @param qe the exception thrown + * @param method the method to call if failover works well + * @param args the arguments of the method + * @return the object return from the method + * @throws Throwable + */ + private Object handleFailOver(QueryException qe, Method method, Object[] args) throws Throwable{ + HandleErrorResult handleErrorResult = listener.handleFailover(method, args); + if (handleErrorResult.mustThrowError) listener.throwFailoverMessage(qe, handleErrorResult.isReconnected); + return handleErrorResult.resultObject; + } + + /** + * Check if this Sqlerror is a connection exception. if that's the case, must be handle by failover + * + * error codes : + * 08000 : connection exception + * 08001 : SQL client unable to establish SQL connection + * 08002 : connection name in use + * 08003 : connection does not exist + * 08004 : SQL server rejected SQL connection + * 08006 : connection failure + * 08007 : transaction resolution unknown + * 70100 : connection was killed + * @param 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") + //|| "70100".equals(e.getSqlState()) + ) { + return true; + } + return false; + } + + public void reconnect() throws SQLException { + try { + listener.reconnect(); + } catch (QueryException e) { + SQLExceptionMapper.throwException(e, null, null); + } + } + + public Listener getListener() { + return listener; + } +} 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..5dfacdc04 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/HandleErrorResult.java @@ -0,0 +1,64 @@ +/* +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 boolean isReconnected = false; + + public Object resultObject = null; + + public HandleErrorResult() { + } + + public HandleErrorResult(boolean isReconnected) { + this.isReconnected = isReconnected; + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/MastersSlavesProtocol.java b/src/main/java/org/mariadb/jdbc/internal/mysql/MastersSlavesProtocol.java new file mode 100644 index 000000000..6806f902e --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/MastersSlavesProtocol.java @@ -0,0 +1,187 @@ +/* +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.mysql.listener.impl.MastersSlavesListener; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.util.*; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MastersSlavesProtocol extends MySQLProtocol { + + private static Logger log = Logger.getLogger(MastersSlavesProtocol.class.getName()); + + boolean masterConnection = false; + boolean mustBeMasterConnection = false; + + public MastersSlavesProtocol(final JDBCUrl url, final ReentrantReadWriteLock lock) { + super(url, lock); + } + + + /** + * loop until found the failed connection. + * + * @param listener current listener + * @param addresses list of HostAddress to loop + * @param blacklist current blacklist + * @param searchFilter search parameter + * @throws QueryException if not found + */ + public static void loop(MastersSlavesListener listener, final List addresses, Map blacklist, SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINE)) { + log.fine("searching for master:" + searchFilter.isSearchForMaster() + " replica:" + searchFilter.isSearchForSlave() + " addresses:" + addresses ); + } + + MastersSlavesProtocol protocol; + List loopAddresses = new LinkedList<>(addresses); + int maxConnectionTry = listener.getRetriesAllDown(); + QueryException lastQueryException = null; + + while (!loopAddresses.isEmpty() || (!searchFilter.isUniqueLoop() && maxConnectionTry > 0)) { + protocol = getNewProtocol(listener.getProxy(), listener.getJdbcUrl()); + + if (listener.isExplicitClosed() || (!listener.isSecondaryHostFail() && !listener.isMasterHostFail())) return; + maxConnectionTry--; + + try { + protocol.setHostAddress(loopAddresses.get(0)); + loopAddresses.remove(0); + + if (log.isLoggable(Level.FINE)) log.fine("trying to connect to " + protocol.getHostAddress()); + log.finest("log **1 " + protocol.getProxy().lock.getReadLockCount()+ " " + protocol.getProxy().lock.getWriteHoldCount()); + + protocol.connect(); + log.finest("log **2 " + protocol.getProxy().lock.getReadLockCount()+ " " + protocol.getProxy().lock.getWriteHoldCount()); + blacklist.remove(protocol.getHostAddress()); + if (log.isLoggable(Level.FINE)) log.fine("connected to " + (protocol.isMasterConnection()?"primary ":"replica ") + protocol.getHostAddress()); + + log.finest("log **3 " + protocol.getProxy().lock.getReadLockCount()+ " " + protocol.getProxy().lock.getWriteHoldCount()); + if (searchFilter.isSearchForMaster() && protocol.isMasterConnection()) { + if (foundMaster(listener, protocol, searchFilter)) return; + } else if (searchFilter.isSearchForSlave() && !protocol.isMasterConnection()) { + if (foundSecondary(listener, protocol, searchFilter)) return; + } else { + protocol.close(); + } + log.finest("log **4 " + protocol.getProxy().lock.getReadLockCount()+ " " + protocol.getProxy().lock.getWriteHoldCount()); + + } catch (QueryException e) { + lastQueryException = e; + blacklist.put(protocol.getHostAddress(), System.currentTimeMillis()); + if (log.isLoggable(Level.FINE)) log.fine("Could not connect to " + protocol.getHostAddress() + " searching: " + searchFilter + " error: " + e.getMessage()); + } + + if (!searchFilter.isSearchForMaster() && !searchFilter.isSearchForSlave()) return; + + //loop is set so + if (loopAddresses.isEmpty() && !searchFilter.isUniqueLoop() && maxConnectionTry > 0) { + loopAddresses = new LinkedList<>(addresses); + listener.checkIfTypeHaveChanged(searchFilter); + } + + } + + if (searchFilter.isSearchForMaster() || searchFilter.isSearchForSlave()) { + String error = "No active connection found for replica"; + if (searchFilter.isSearchForMaster()) error = "No active connection found for master"; + if (lastQueryException != null) { + throw new QueryException(error, lastQueryException.getErrorCode(), lastQueryException.getSqlState(), lastQueryException); + } + throw new QueryException(error); + } + + } + + private static boolean foundMaster(MastersSlavesListener listener, MastersSlavesProtocol protocol,SearchFilter searchFilter) { + protocol.setMustBeMasterConnection(true); + searchFilter.setSearchForMaster(false); + listener.foundActiveMaster(protocol); + if (!searchFilter.isSearchForSlave()) return true; + else { + if (listener.isExplicitClosed() + || searchFilter.isFineIfFoundOnlyMaster() + || !listener.isSecondaryHostFail()) return true; + } + return false; + } + + private static boolean foundSecondary(MastersSlavesListener listener, MastersSlavesProtocol protocol,SearchFilter searchFilter) { + searchFilter.setSearchForSlave(false); + protocol.setMustBeMasterConnection(false); + listener.foundActiveSecondary(protocol); + if (!searchFilter.isSearchForMaster()) return true; + else { + if (listener.isExplicitClosed() + || searchFilter.isFineIfFoundOnlySlave() + || !listener.isMasterHostFail()) return true; + } + return false; + } + + public static MastersSlavesProtocol getNewProtocol(FailoverProxy proxy, JDBCUrl jdbcUrl) { + MastersSlavesProtocol newProtocol = new MastersSlavesProtocol(jdbcUrl, proxy.lock); + newProtocol.setProxy(proxy); + return newProtocol; + } + + public boolean mustBeMasterConnection() { + return mustBeMasterConnection; + } + public void setMustBeMasterConnection(boolean mustBeMasterConnection) { + this.mustBeMasterConnection = mustBeMasterConnection; + } +} 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 8f8675f89..4119bd754 100644 --- a/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/MySQLProtocol.java @@ -64,6 +64,8 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.mariadb.jdbc.internal.common.query.MySQLQuery; import org.mariadb.jdbc.internal.common.query.Query; import org.mariadb.jdbc.internal.common.queryresults.*; +import org.mariadb.jdbc.internal.mysql.listener.Listener; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; import org.mariadb.jdbc.internal.mysql.packet.MySQLGreetingReadPacket; import org.mariadb.jdbc.internal.mysql.packet.commands.*; @@ -81,10 +83,9 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; +import java.sql.Connection; import java.util.*; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; @@ -95,12 +96,12 @@ class MyX509TrustManager implements X509TrustManager { String serverCertFile; X509TrustManager trustManager; - public MyX509TrustManager(Properties props) throws Exception{ - boolean trustServerCertificate = props.getProperty("trustServerCertificate") != null; + public MyX509TrustManager(Options options) throws Exception{ + boolean trustServerCertificate = options.trustServerCertificate; if (trustServerCertificate) return; - serverCertFile = props.getProperty("serverSslCert"); + serverCertFile = options.serverSslCert; InputStream inStream; if (serverCertFile.startsWith("-----BEGIN CERTIFICATE-----")) { @@ -152,90 +153,53 @@ public X509Certificate[] getAcceptedIssuers() { } } -public class MySQLProtocol { +public class MySQLProtocol implements Protocol { private final static Logger log = Logger.getLogger(MySQLProtocol.class.getName()); + protected final ReentrantReadWriteLock lock; private boolean connected = false; - private Socket socket; - private PacketOutputStream writer; + private boolean explicitClosed = false; + protected Socket socket; + protected PacketOutputStream writer; private String version; - private boolean readOnly = false; + protected 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; private long serverThreadId; public boolean moreResults = false; public boolean hasWarnings = false; public StreamingSelectResult activeResult= null; public int datatypeMappingFlags; public short serverStatus; - JDBCUrl jdbcUrl; - HostAddress currentHost; - + protected final JDBCUrl jdbcUrl; + protected HostAddress currentHost; + protected FailoverProxy proxy; private int majorVersion; private int minorVersion; private int patchVersion; - - boolean hostFailed; - long failTimestamp; - int reconnectCount; - int queriesSinceFailover; + private int maxAllowedPacket; private byte serverLanguage; + private int transactionIsolationLevel=0; + boolean hostFailed; /* =========================== 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 InputStream localInfileInputStream; private SSLSocketFactory getSSLSocketFactory(boolean trustServerCertificate) throws QueryException { - if (info.getProperty("trustServerCertificate") == null - && info.getProperty("serverSslCert") == null) { + if (jdbcUrl.getOptions().trustServerCertificate + && jdbcUrl.getOptions().serverSslCert == null) { return (SSLSocketFactory)SSLSocketFactory.getDefault(); } try { SSLContext sslContext = SSLContext.getInstance("TLS"); - X509TrustManager[] m = {new MyX509TrustManager(info)}; + X509TrustManager[] m = {new MyX509TrustManager(jdbcUrl.getOptions())}; sslContext.init(null, m ,null); - return sslContext.getSocketFactory(); + return sslContext.getSocketFactory(); } catch (Exception e) { throw new QueryException(e.getMessage(),0, "HY000", e); } @@ -243,275 +207,190 @@ private SSLSocketFactory getSSLSocketFactory(boolean trustServerCertificate) th } /** * Get a protocol instance - * @param url connection URL - * @param username the username - * @param password the password - * @param info - * @throws org.mariadb.jdbc.internal.common.QueryException - * if there is a problem reading / sending the packets - * @throws SQLException + * @param jdbcUrl connection URL infos + * @param lock the lock for thread synchronisation */ - public MySQLProtocol(JDBCUrl url, - final String username, - final String password, - Properties info) - throws QueryException, SQLException { - String fractionalSeconds = info.getProperty("useFractionalSeconds", "true"); - if ("true".equalsIgnoreCase(fractionalSeconds)) { - info.setProperty("useFractionalSeconds", "true"); - } - if ("true".equalsIgnoreCase(info.getProperty("pinGlobalTxToPhysicalConnection", "false"))) { - info.setProperty("pinGlobalTxToPhysicalConnection", "true"); - } - this.info = info; - this.jdbcUrl = url; - this.database = (jdbcUrl.getDatabase() == null ? "" : jdbcUrl.getDatabase()); - this.username = (username == null ? "" : username); - this.password = (password == null ? "" : password); - - String logLevel = info.getProperty("MySQLProtocolLogLevel"); - if (logLevel != null) - log.setLevel(Level.parse(logLevel)); - else - log.setLevel(Level.OFF); + public MySQLProtocol(final JDBCUrl jdbcUrl, final ReentrantReadWriteLock lock) { + this.lock = lock; + this.jdbcUrl = jdbcUrl; + this.database = (jdbcUrl.getDatabase() == null ? "" : jdbcUrl.getDatabase()); + this.username = (jdbcUrl.getUsername() == null ? "" : jdbcUrl.getUsername()); + this.password = (jdbcUrl.getPassword() == null ? "" : jdbcUrl.getPassword()); 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 * * @throws QueryException : handshake error, e.g wrong user or password * @throws IOException : connection error (host/port not available) - * @throws SQLException */ - void connect(String host, int port) throws QueryException, IOException, SQLException{ + private void connect(String host, int port) throws QueryException, IOException{ + SocketFactory socketFactory = null; - String socketFactoryName = info.getProperty("socketFactory"); + String socketFactoryName = jdbcUrl.getOptions().socketFactory; if (socketFactoryName != null) { try { socketFactory = (SocketFactory) (Class.forName(socketFactoryName).newInstance()); } catch (Exception sfex){ - log.info("Failed to create socket factory " + socketFactoryName); + log.fine("Failed to create socket factory " + socketFactoryName); socketFactory = SocketFactory.getDefault(); } } else { socketFactory = SocketFactory.getDefault(); } - // Extract connectTimeout URL parameter - String connectTimeoutString = info.getProperty("connectTimeout"); - Integer connectTimeout = null; - if (connectTimeoutString != null) { - try { - connectTimeout = Integer.valueOf(connectTimeoutString); - } catch (Exception e) { - connectTimeout = null; - } - } // Create socket with timeout if required - if (info.getProperty("pipe") != null) { - socket = new org.mariadb.jdbc.internal.mysql.NamedPipeSocket(host, info.getProperty("pipe")); - } else if(info.getProperty("localSocket") != null){ + if (jdbcUrl.getOptions().pipe != null) { + socket = new org.mariadb.jdbc.internal.mysql.NamedPipeSocket(host, jdbcUrl.getOptions().pipe); + } else if(jdbcUrl.getOptions().localSocket != null){ try { - socket = new org.mariadb.jdbc.internal.mysql.UnixDomainSocket(info.getProperty("localSocket")); + socket = new org.mariadb.jdbc.internal.mysql.UnixDomainSocket(jdbcUrl.getOptions().localSocket); } catch( RuntimeException re) { - // could be e.g library loading error + // could be e.g library loading error throw new IOException(re.getMessage(),re.getCause()); } - } else if(info.getProperty("sharedMemory")!= null) { + } else if(jdbcUrl.getOptions().sharedMemory != null) { try { - socket = new SharedMemorySocket(info.getProperty("sharedMemory")); + socket = new SharedMemorySocket(jdbcUrl.getOptions().sharedMemory); } catch( RuntimeException re) { - // could be e.g library loading error - throw new IOException(re.getMessage(),re.getCause()); + // could be e.g library loading error + throw new IOException(re.getMessage(),re.getCause()); } } else { socket = socketFactory.createSocket(); } try { - String value = info.getProperty("tcpNoDelay", "false"); - if (value.equalsIgnoreCase("true")) - socket.setTcpNoDelay(true); - - value = info.getProperty("tcpKeepAlive", "false"); - if (value.equalsIgnoreCase("true")) - socket.setKeepAlive(true); - - value = info.getProperty("tcpRcvBuf"); - if (value != null) - socket.setReceiveBufferSize(Integer.parseInt(value)); - - value = info.getProperty("tcpSndBuf"); - if (value != null) - socket.setSendBufferSize(Integer.parseInt(value)); - - value = info.getProperty("tcpAbortiveClose","false"); - if (value.equalsIgnoreCase("true")) - socket.setSoLinger(true, 0); - - } catch (Exception e) { - log.finest("Failed to set socket option: " + e.getLocalizedMessage()); - } - - // Bind the socket to a particular interface if the connection property - // localSocketAddress has been defined. - String localHost = info.getProperty("localSocketAddress"); - if (localHost != null) { - InetSocketAddress localAddress = new InetSocketAddress(localHost, 0); - socket.bind(localAddress); - } - - if (!socket.isConnected()) { + if (jdbcUrl.getOptions().tcpNoDelay) socket.setTcpNoDelay(true); + if (jdbcUrl.getOptions().tcpKeepAlive) socket.setKeepAlive(true); + if (jdbcUrl.getOptions().tcpRcvBuf != null) socket.setReceiveBufferSize(jdbcUrl.getOptions().tcpRcvBuf); + if (jdbcUrl.getOptions().tcpSndBuf != null) socket.setSendBufferSize(jdbcUrl.getOptions().tcpSndBuf); + if (jdbcUrl.getOptions().tcpAbortiveClose) socket.setSoLinger(true, 0); + } catch (Exception e) { + if (log.isLoggable(Level.FINE))log.fine("Failed to set socket option: " + e.getLocalizedMessage()); + } + + // Bind the socket to a particular interface if the connection property + // localSocketAddress has been defined. + if (jdbcUrl.getOptions().localSocketAddress != null) { + InetSocketAddress localAddress = new InetSocketAddress(jdbcUrl.getOptions().localSocketAddress, 0); + socket.bind(localAddress); + } + + if (!socket.isConnected()) { InetSocketAddress sockAddr = new InetSocketAddress(host, port); - if (connectTimeout != null) { - socket.connect(sockAddr, connectTimeout); + if (jdbcUrl.getOptions().connectTimeout != null) { + socket.connect(sockAddr, jdbcUrl.getOptions().connectTimeout); } else { socket.connect(sockAddr); } - } + } - // Extract socketTimeout URL parameter - String socketTimeoutString = info.getProperty("socketTimeout"); - Integer socketTimeout = null; - if (socketTimeoutString != null) { - try { - socketTimeout = Integer.valueOf(socketTimeoutString); - } catch (Exception e) { - socketTimeout = null; - } - } - if (socketTimeout != null) - socket.setSoTimeout(socketTimeout); - - try { - InputStream reader; - reader = new BufferedInputStream(socket.getInputStream(), 32768); - packetFetcher = new SyncPacketFetcher(reader); - writer = new PacketOutputStream(socket.getOutputStream()); - RawPacket packet = packetFetcher.getRawPacket(); - if (ReadUtil.isErrorPacket(packet)) { - reader.close(); - ErrorPacket errorPacket = (ErrorPacket)ResultPacketFactory.createResultPacket(packet); - throw new QueryException(errorPacket.getMessage()); - } - final MySQLGreetingReadPacket greetingPacket = new MySQLGreetingReadPacket(packet); - this.serverThreadId = greetingPacket.getServerThreadID(); - this.serverLanguage = greetingPacket.getServerLanguage(); - boolean useCompression = false; - - log.finest("Got greeting packet"); - this.version = greetingPacket.getServerVersion(); - parseVersion(); - byte packetSeq = 1; - int capabilities = - MySQLServerCapabilities.LONG_PASSWORD | - MySQLServerCapabilities.IGNORE_SPACE | - MySQLServerCapabilities.CLIENT_PROTOCOL_41| - MySQLServerCapabilities.TRANSACTIONS| - MySQLServerCapabilities.SECURE_CONNECTION| - MySQLServerCapabilities.LOCAL_FILES| - MySQLServerCapabilities.MULTI_RESULTS| - MySQLServerCapabilities.FOUND_ROWS; - - - - if(info.getProperty("allowMultiQueries") != null - || (info.getProperty("rewriteBatchedStatements") != null - && "true".equalsIgnoreCase(info.getProperty("rewriteBatchedStatements")))) { - capabilities |= MySQLServerCapabilities.MULTI_STATEMENTS; - } - if(info.getProperty("useCompression") != null) { - capabilities |= MySQLServerCapabilities.COMPRESS; - useCompression = true; - } - if(info.getProperty("interactiveClient") != null) { - capabilities |= MySQLServerCapabilities.CLIENT_INTERACTIVE; - } - // If a database is given, but createDB is not defined or is false, - // then just try to connect to the given database - if (database != null && !createDB()) - capabilities |= MySQLServerCapabilities.CONNECT_WITH_DB; - - if(info.getProperty("useSSL") != null && - (greetingPacket.getServerCapabilities() & MySQLServerCapabilities.SSL) != 0 ) { - capabilities |= MySQLServerCapabilities.SSL; - AbbreviatedMySQLClientAuthPacket amcap = new AbbreviatedMySQLClientAuthPacket(capabilities); - amcap.send(writer); - - boolean trustServerCertificate = info.getProperty("trustServerCertificate") != null; - - SSLSocketFactory f = getSSLSocketFactory(trustServerCertificate); - SSLSocket sslSocket = (SSLSocket)f.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), false); - - sslSocket.setEnabledProtocols(new String [] {"TLSv1"}); - sslSocket.setUseClientMode(true); - sslSocket.startHandshake(); - socket = sslSocket; - writer = new PacketOutputStream(socket.getOutputStream()); - reader = new BufferedInputStream(socket.getInputStream(), 32768); - packetFetcher = new SyncPacketFetcher(reader); - - packetSeq++; - } else if(info.getProperty("useSSL") != null){ - throw new QueryException("Trying to connect with ssl, but ssl not enabled in the server"); - } + // Extract socketTimeout URL parameter + if (jdbcUrl.getOptions().socketTimeout != null) socket.setSoTimeout(jdbcUrl.getOptions().socketTimeout); + + try { + InputStream reader; + reader = new BufferedInputStream(socket.getInputStream(), 32768); + packetFetcher = new SyncPacketFetcher(reader); + writer = new PacketOutputStream(socket.getOutputStream()); + RawPacket packet = packetFetcher.getRawPacket(); + if (ReadUtil.isErrorPacket(packet)) { + reader.close(); + ErrorPacket errorPacket = (ErrorPacket)ResultPacketFactory.createResultPacket(packet); + throw new QueryException(errorPacket.getMessage()); + } + final MySQLGreetingReadPacket greetingPacket = new MySQLGreetingReadPacket(packet); + this.serverThreadId = greetingPacket.getServerThreadID(); + this.serverLanguage = greetingPacket.getServerLanguage(); + + this.version = greetingPacket.getServerVersion(); + parseVersion(); + byte packetSeq = 1; + int capabilities = + MySQLServerCapabilities.LONG_PASSWORD | + MySQLServerCapabilities.IGNORE_SPACE | + MySQLServerCapabilities.CLIENT_PROTOCOL_41| + MySQLServerCapabilities.TRANSACTIONS| + MySQLServerCapabilities.SECURE_CONNECTION| + MySQLServerCapabilities.LOCAL_FILES| + MySQLServerCapabilities.MULTI_RESULTS| + MySQLServerCapabilities.FOUND_ROWS; + + + if(jdbcUrl.getOptions().allowMultiQueries || (jdbcUrl.getOptions().rewriteBatchedStatements)) { + capabilities |= MySQLServerCapabilities.MULTI_STATEMENTS; + } - final MySQLClientAuthPacket cap = new MySQLClientAuthPacket(this.username, - this.password, - database, - capabilities, - decideLanguage(), - greetingPacket.getSeed(), - packetSeq); - cap.send(writer); - log.finest("Sending auth packet"); + if(jdbcUrl.getOptions().useCompression) capabilities |= MySQLServerCapabilities.COMPRESS; + if(jdbcUrl.getOptions().interactiveClient) capabilities |= MySQLServerCapabilities.CLIENT_INTERACTIVE; + + // If a database is given, but createDatabaseIfNotExist is not defined or is false, + // then just try to connect to the given database + if (database != null && !jdbcUrl.getOptions().createDatabaseIfNotExist) + capabilities |= MySQLServerCapabilities.CONNECT_WITH_DB; + + if(jdbcUrl.getOptions().useSSL && + (greetingPacket.getServerCapabilities() & MySQLServerCapabilities.SSL) != 0 ) { + capabilities |= MySQLServerCapabilities.SSL; + AbbreviatedMySQLClientAuthPacket amcap = new AbbreviatedMySQLClientAuthPacket(capabilities); + amcap.send(writer); + + SSLSocketFactory f = getSSLSocketFactory(jdbcUrl.getOptions().trustServerCertificate); + SSLSocket sslSocket = (SSLSocket)f.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), false); + + sslSocket.setEnabledProtocols(new String [] {"TLSv1"}); + sslSocket.setUseClientMode(true); + sslSocket.startHandshake(); + socket = sslSocket; + writer = new PacketOutputStream(socket.getOutputStream()); + reader = new BufferedInputStream(socket.getInputStream(), 32768); + packetFetcher = new SyncPacketFetcher(reader); + + packetSeq++; + } else if(jdbcUrl.getOptions().useSSL){ + throw new QueryException("Trying to connect with ssl, but ssl not enabled in the server"); + } - RawPacket rp = packetFetcher.getRawPacket(); + final MySQLClientAuthPacket cap = new MySQLClientAuthPacket(this.username, + this.password, + database, + capabilities, + decideLanguage(), + greetingPacket.getSeed(), + packetSeq); + cap.send(writer); - if ((rp.getByteBuffer().get(0) & 0xFF) == 0xFE) { // Server asking for old format password - final MySQLClientOldPasswordAuthPacket oldPassPacket = new MySQLClientOldPasswordAuthPacket( - this.password, Utils.copyWithLength(greetingPacket.getSeed(), - 8), rp.getPacketSeq() + 1); - oldPassPacket.send(writer); + RawPacket rp = packetFetcher.getRawPacket(); - rp = packetFetcher.getRawPacket(); - } + if ((rp.getByteBuffer().get(0) & 0xFF) == 0xFE) { // Server asking for old format password + final MySQLClientOldPasswordAuthPacket oldPassPacket = new MySQLClientOldPasswordAuthPacket( + this.password, Utils.copyWithLength(greetingPacket.getSeed(), + 8), rp.getPacketSeq() + 1); + oldPassPacket.send(writer); - checkErrorPacket(rp); - ResultPacket resultPacket = ResultPacketFactory.createResultPacket(rp); - OKPacket ok = (OKPacket)resultPacket; - serverStatus = ok.getServerStatus(); + rp = packetFetcher.getRawPacket(); + } - if (useCompression) { - writer = new PacketOutputStream(new CompressOutputStream(socket.getOutputStream())); - packetFetcher = new SyncPacketFetcher(new DecompressInputStream(socket.getInputStream())); - } + checkErrorPacket(rp); + ResultPacket resultPacket = ResultPacketFactory.createResultPacket(rp); + OKPacket ok = (OKPacket)resultPacket; + serverStatus = ok.getServerStatus(); - // In JDBC, connection must start in autocommit mode. - if ((serverStatus & ServerStatus.AUTOCOMMIT) == 0) { - executeQuery(new MySQLQuery("set autocommit=1")); - } + if (jdbcUrl.getOptions().useCompression) { + writer = new PacketOutputStream(new CompressOutputStream(socket.getOutputStream())); + packetFetcher = new SyncPacketFetcher(new DecompressInputStream(socket.getInputStream())); + } + + // In JDBC, connection must start in autocommit mode. + if ((serverStatus & ServerStatus.AUTOCOMMIT) == 0) { + executeQuery(new MySQLQuery("set autocommit=1")); + } SelectQueryResult qr = null; try { qr = (SelectQueryResult) executeQuery(new MySQLQuery("show variables like 'max_allowed_packet'")); @@ -521,47 +400,50 @@ void connect(String host, int port) throws QueryException, IOException, SQLExcep if (qr != null)qr.close(); } - String sessionVariables = info.getProperty("sessionVariables"); - if (sessionVariables != null) { - executeQuery(new MySQLQuery("set session " + sessionVariables)); - } + if (jdbcUrl.getOptions().sessionVariables != null) executeQuery(new MySQLQuery("set session " + jdbcUrl.getOptions().sessionVariables)); - // 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)); - } + // 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 (checkIfMaster()) { + if (jdbcUrl.getOptions().createDatabaseIfNotExist) { + // 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; + } catch (IOException e) { + throw new QueryException("Could not connect to " + host + ":" + + port + ": " + e.getMessage(), + -1, + SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), + e); + } + + } + + public boolean checkIfMaster() throws QueryException { + return isMasterConnection(); + } - activeResult = null; - moreResults = false; - hasWarnings = false; - connected = true; - hostFailed = false; // Prevent reconnects - } catch (IOException e) { - throw new QueryException("Could not connect to " + host + ":" + - port + ": " + e.getMessage(), - -1, - SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), - e); - } - - } - private boolean isServerLanguageUTF8MB4(byte serverLanguage) { - Byte[] utf8mb4Languages = { - (byte)45,(byte)46,(byte)224,(byte)225,(byte)226,(byte)227,(byte)228, - (byte)229,(byte)230,(byte)231,(byte)232,(byte)233,(byte)234,(byte)235, - (byte)236,(byte)237,(byte)238,(byte)239,(byte)240,(byte)241,(byte)242, - (byte)243,(byte)245 - }; - return Arrays.asList(utf8mb4Languages).contains(serverLanguage); + Byte[] utf8mb4Languages = { + (byte)45,(byte)46,(byte)224,(byte)225,(byte)226,(byte)227,(byte)228, + (byte)229,(byte)230,(byte)231,(byte)232,(byte)233,(byte)234,(byte)235, + (byte)236,(byte)237,(byte)238,(byte)239,(byte)240,(byte)241,(byte)242, + (byte)243,(byte)245 + }; + return Arrays.asList(utf8mb4Languages).contains(serverLanguage); } private byte decideLanguage() { - byte result = (byte) (isServerLanguageUTF8MB4(this.serverLanguage) ? this.serverLanguage : 33); - return result; + byte result = (byte) (isServerLanguageUTF8MB4(this.serverLanguage) ? this.serverLanguage : 33); + return result; } void checkErrorPacket(RawPacket rp) throws QueryException{ @@ -571,34 +453,34 @@ void checkErrorPacket(RawPacket rp) throws QueryException{ throw new QueryException("Could not connect: " + message); } } - - + + void readEOFPacket() throws QueryException, IOException { RawPacket rp = packetFetcher.getRawPacket(); checkErrorPacket(rp); ResultPacket resultPacket = ResultPacketFactory.createResultPacket(rp); if (resultPacket.getResultType() != ResultPacket.ResultType.EOF) { - throw new QueryException("Unexpected packet type " + resultPacket.getResultType() + + throw new QueryException("Unexpected packet type " + resultPacket.getResultType() + "insted of EOF"); } EOFPacket eof = (EOFPacket)resultPacket; this.hasWarnings = eof.getWarningCount() > 0; this.serverStatus = eof.getStatusFlags(); } - + void readOKPacket() throws QueryException, IOException { RawPacket rp = packetFetcher.getRawPacket(); checkErrorPacket(rp); ResultPacket resultPacket = ResultPacketFactory.createResultPacket(rp); if (resultPacket.getResultType() != ResultPacket.ResultType.OK) { - throw new QueryException("Unexpected packet type " + resultPacket.getResultType() + + throw new QueryException("Unexpected packet type " + resultPacket.getResultType() + "insted of OK"); } OKPacket ok = (OKPacket)resultPacket; this.hasWarnings = ok.getWarnings() > 0; this.serverStatus = ok.getServerStatus(); } - + public class PrepareResult { public int statementId; public MySQLColumnInformation[] columns; @@ -610,13 +492,14 @@ public PrepareResult(int statementId, MySQLColumnInformation[] columns, MySQLCo } } + @Override public PrepareResult prepare(String sql) throws QueryException { try { writer.startPacket(0); writer.write(0x16); writer.write(sql.getBytes("UTF8")); writer.finishPacket(); - + RawPacket rp = packetFetcher.getRawPacket(); checkErrorPacket(rp); byte b = rp.getByteBuffer().get(0); @@ -643,7 +526,7 @@ public PrepareResult prepare(String sql) throws QueryException { } readEOFPacket(); } - + return new PrepareResult(statementId,columns,params); } else { throw new QueryException("Unexpected packet returned by server, first byte " + b); @@ -651,153 +534,202 @@ public PrepareResult prepare(String sql) throws QueryException { } catch (IOException e) { throw new QueryException(e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), - e); + e); } } - - public synchronized void closePreparedStatement(int statementId) throws QueryException{ + + @Override + public void closePreparedStatement(int statementId) throws QueryException { + lock.writeLock().lock(); try { writer.startPacket(0); writer.write(0x19); /*COM_STMT_CLOSE*/ - writer.write(statementId); + writer.write(statementId); writer.finishPacket(); } catch(IOException e) { throw new QueryException(e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } finally { + lock.writeLock().unlock(); } } - public void setHostFailed() { + + public void setHostFailedWithoutProxy() { hostFailed = true; - failTimestamp = System.currentTimeMillis(); + close(); } + public JDBCUrl getJdbcUrl() { + return jdbcUrl; + } - public boolean shouldReconnect() { - return (!inTransaction() && hostFailed && autoReconnect && reconnectCount < maxReconnects); + public static MySQLProtocol getNewProtocol(FailoverProxy proxy, JDBCUrl jdbcUrl) { + MySQLProtocol newProtocol = new MySQLProtocol(jdbcUrl, proxy.lock); + newProtocol.setProxy(proxy); + return newProtocol; } + @Override public boolean getAutocommit() { - return ((serverStatus & ServerStatus.AUTOCOMMIT) != 0); + lock.readLock().lock(); + try { + return ((serverStatus & ServerStatus.AUTOCOMMIT) != 0); + } finally { + lock.readLock().unlock(); + } + } + public boolean isMasterConnection() { + return ParameterConstant.TYPE_MASTER.equals(currentHost.type); + } + + public boolean mustBeMasterConnection() { 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; + lock.readLock().lock(); try { - connect(addrs[0].host, addrs[0].port); - try { - close(saveFetcher, saveWriter, saveSocket); - } catch (Exception e) { - } - success = true; + return ((serverStatus & ServerStatus.NO_BACKSLASH_ESCAPES) != 0); } finally { - if (!success) { - failTimestamp = System.currentTimeMillis(); - queriesSinceFailover = 0; - this.packetFetcher = saveFetcher; - this.writer = saveWriter; - this.socket = saveSocket; - } + lock.readLock().unlock(); } } - public void connect() throws QueryException, SQLException { + + public void connect() throws QueryException { if (!isClosed()) { close(); } + try { + connect(currentHost.host, currentHost.port); + return; + } catch (IOException e) { + throw new QueryException("Could not connect to " + currentHost + "." + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } + } - HostAddress[] addrs = jdbcUrl.getHostAddresses(); + public void connectWithoutProxy() throws QueryException { + if (!isClosed()) { + close(); + } + + List 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]; + for(int i = 0; i < addrs.size(); i++) { + currentHost = addrs.get(i); try { connect(currentHost.host, currentHost.port); return; } catch (IOException e) { - if (i == addrs.length - 1) { + if (i == addrs.size() - 1) { throw new QueryException("Could not connect to " + HostAddress.toString(addrs) + - " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + " : " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); } } } } - public boolean isMasterConnection() { - return currentHost == jdbcUrl.getHostAddresses()[0]; + public boolean shouldReconnectWithoutProxy() { + return (!inTransaction() && hostFailed && jdbcUrl.getOptions().autoReconnect); } /** - * Check if fail back to master connection is desired, - * @return + * loop until found the failed connection. + * + * @param listener current listener + * @param addresses list of HostAddress to loop + * @param blacklist current blacklist + * @param searchFilter search parameter + * @throws QueryException if not found */ - public boolean shouldTryFailback() { - if (isMasterConnection()) - return false; + public static void loop(Listener listener, final List addresses, Map blacklist, SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINE)) { + log.fine("searching for master:" + searchFilter.isSearchForMaster() + " replica:" + searchFilter.isSearchForSlave() + " addresses:" + addresses ); + } - if (inTransaction()) - return false; - if (reconnectCount >= maxReconnects) - return false; + MySQLProtocol protocol; + List loopAddresses = new LinkedList<>(addresses); + int maxConnectionTry = listener.getRetriesAllDown(); + QueryException lastQueryException = null; + while (!loopAddresses.isEmpty() || (!searchFilter.isUniqueLoop() && maxConnectionTry > 0)) { + protocol = getNewProtocol(listener.getProxy(), listener.getJdbcUrl()); - long now = System.currentTimeMillis(); - if ((now - failTimestamp)/1000 > secondsBeforeRetryMaster) - return true; - if (queriesSinceFailover > queriesBeforeRetryMaster) - return true; - return false; + if (listener.isExplicitClosed()) return; + maxConnectionTry--; + + try { + protocol.setHostAddress(loopAddresses.get(0)); + loopAddresses.remove(0); + + if (log.isLoggable(Level.FINE)) log.fine("trying to connect to " + protocol.getHostAddress()); + protocol.connect(); + blacklist.remove(protocol.getHostAddress()); + if (log.isLoggable(Level.FINE)) log.fine("connected to primary " + protocol.getHostAddress()); + listener.foundActiveMaster(protocol); + return; + + } catch (QueryException e) { + blacklist.put(protocol.getHostAddress(), System.currentTimeMillis()); + if (log.isLoggable(Level.FINE)) log.fine("Could not connect to " + protocol.getHostAddress() + " searching: " + searchFilter + " error: " + e.getMessage()); + lastQueryException = e; + } + + //loop is set so + if (loopAddresses.isEmpty() && !searchFilter.isUniqueLoop() && maxConnectionTry > 0) { + loopAddresses = new LinkedList<>(addresses); + } + } + if (lastQueryException != null) { + throw new QueryException("No active connection found for master", lastQueryException.getErrorCode(), lastQueryException.getSqlState(), lastQueryException); + } + throw new QueryException("No active connection found for master"); } - public boolean inTransaction() - { - return ((serverStatus & ServerStatus.IN_TRANSACTION) != 0); + @Override + public boolean inTransaction() { + lock.readLock().lock(); + try { + return ((serverStatus & ServerStatus.IN_TRANSACTION) != 0); + } finally { + lock.readLock().unlock(); + } } private void setDatatypeMappingFlags() { datatypeMappingFlags = 0; - String tinyInt1isBit = info.getProperty("tinyInt1isBit"); - String yearIsDateType = info.getProperty("yearIsDateType"); - - if (tinyInt1isBit == null || tinyInt1isBit.equals("1") || tinyInt1isBit.equals("true")) { - datatypeMappingFlags |= MySQLValueObject.TINYINT1_IS_BIT; - } - if (yearIsDateType == null || yearIsDateType.equals("1") || yearIsDateType.equals("true")) { - datatypeMappingFlags |= MySQLValueObject.YEAR_IS_DATE_TYPE; - } + if (jdbcUrl.getOptions().tinyInt1isBit) datatypeMappingFlags |= MySQLValueObject.TINYINT1_IS_BIT; + if (jdbcUrl.getOptions().yearIsDateType) datatypeMappingFlags |= MySQLValueObject.YEAR_IS_DATE_TYPE; } - public Properties getInfo() { - return info; + @Override + public Options getOptions() { + return jdbcUrl.getOptions(); } + void skip() throws IOException, QueryException{ - if (activeResult != null) { - activeResult.close(); - } + if (activeResult != null) { + activeResult.close(); + } - while (moreResults) { + while (moreResults) { getMoreResults(true); - } + } } + @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 { - closePacket.send(packetOutputStream); + closePacket.send(packetOutputStream); socket.shutdownOutput(); socket.setSoTimeout(3); InputStream is = socket.getInputStream(); @@ -807,10 +739,10 @@ private static void close(PacketFetcher fetcher, PacketOutputStream packetOutput packetOutputStream.close(); fetcher.close(); } catch (IOException e) { - throw new QueryException("Could not close connection: " + e.getMessage(), - -1, - SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), - e); + throw new QueryException("Could not close connection: " + e.getMessage(), + -1, + SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), + e); } finally { try { socket.close(); @@ -819,32 +751,53 @@ private static void close(PacketFetcher fetcher, PacketOutputStream packetOutput } } } + + + public void closeExplicit() { + this.explicitClosed = true; + close(); + } /** * Closes socket and stream readers/writers * Attempts graceful shutdown. */ + @Override public void close() { + if (lock != null) lock.writeLock().lock(); try { - /* If a streaming result set is open, close it.*/ + /* If a streaming result set is open, close it.*/ skip(); } catch (Exception e) { /* eat exception */ } try { - close(packetFetcher,writer, socket); - } - catch (Exception e) { + if (log.isLoggable(Level.FINEST)) log.finest("Closing connection " + currentHost); + 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 { + log.fine("got exception " + e + " while closing connection"); + } finally { this.connected = false; + + if (lock != null) lock.writeLock().unlock(); + } + } + + public void rollback() { + lock.writeLock().lock(); + try { + if (inTransaction()) executeQuery(new MySQLQuery("ROLLBACK")); + } catch (Exception e) { + /* eat exception */ + } finally { + lock.writeLock().unlock(); } } /** * @return true if the connection is closed */ + @Override public boolean isClosed() { return !this.connected; } @@ -865,77 +818,114 @@ private SelectQueryResult createQueryResult(final ResultSetPacket packet, boolea return CachedSelectResult.createCachedSelectResult(streamingResult); } - public void selectDB(final String database) throws QueryException { + @Override + public void setCatalog(final String database) throws QueryException { + lock.writeLock().lock(); log.finest("Selecting db " + database); final SelectDBPacket packet = new SelectDBPacket(database); try { packet.send(writer); final RawPacket rawPacket = packetFetcher.getRawPacket(); - ResultPacketFactory.createResultPacket(rawPacket); + ResultPacket rs = ResultPacketFactory.createResultPacket(rawPacket); + if (rs.getResultType() == ResultPacket.ResultType.ERROR) { + throw new QueryException("Could not select database '" + database +"' : "+ ((ErrorPacket) rs).getMessage()); + } + this.database = database; } catch (IOException e) { - throw new QueryException("Could not select database: " + e.getMessage(), + throw new QueryException("Could not select database '" + database +"' :"+ e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); + } finally { + lock.writeLock().unlock(); } - this.database = database; } + @Override public String getServerVersion() { return version; } + @Override public void setReadonly(final boolean readOnly) { this.readOnly = readOnly; } + @Override public boolean getReadonly() { return readOnly; } + @Override + public HostAddress getHostAddress() { + return currentHost; + } + public void setHostAddress(HostAddress host) { + this.currentHost = host; + this.readOnly = ParameterConstant.TYPE_SLAVE.equals(this.currentHost.type); + } + @Override public String getHost() { return currentHost.host; } + public void setProxy(FailoverProxy proxy) { + this.proxy = proxy; + } + public FailoverProxy getProxy() { + return proxy; + } + @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(); + lock.writeLock().lock(); try { - pingPacket.send(writer); - log.finest("Sent ping packet"); - final RawPacket rawPacket = packetFetcher.getRawPacket(); - return ResultPacketFactory.createResultPacket(rawPacket).getResultType() == ResultPacket.ResultType.OK; - } catch (IOException e) { - throw new QueryException("Could not ping: " + e.getMessage(), - -1, - SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), - e); + final MySQLPingPacket pingPacket = new MySQLPingPacket(); + try { + pingPacket.send(writer); + if (log.isLoggable(Level.FINEST))log.finest("Sent ping packet"); + final RawPacket rawPacket = packetFetcher.getRawPacket(); + return ResultPacketFactory.createResultPacket(rawPacket).getResultType() == ResultPacket.ResultType.OK; + } catch (IOException e) { + throw new QueryException("Could not ping: " + e.getMessage(), + -1, + SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), + e); + } + } finally { + lock.writeLock().unlock(); } } - public QueryResult executeQuery(Query dQuery) throws QueryException, SQLException { - return executeQuery(dQuery, false); + @Override + public QueryResult executeQuery(Query dQuery) throws QueryException { + return executeQuery(dQuery, false); } + @Override + public QueryResult getResult(List dQueries, boolean streaming) throws QueryException { - public QueryResult getResult(List dQueries, boolean streaming) throws QueryException{ - RawPacket rawPacket; + RawPacket rawPacket; ResultPacket resultPacket; try { rawPacket = packetFetcher.getRawPacket(); @@ -943,39 +933,38 @@ public QueryResult getResult(List dQueries, boolean streaming) throws Que if (resultPacket.getResultType() == ResultPacket.ResultType.LOCALINFILE) { // Server request the local file (LOCAL DATA LOCAL INFILE) - // We do accept general URLs, too. If the localInfileStream is - // set, use that. + // We do accept general URLs, too. If the localInfileStream is + // set, use that. InputStream is; if (localInfileInputStream == null) { - LocalInfilePacket localInfilePacket= (LocalInfilePacket)resultPacket; - log.fine("sending local file " + localInfilePacket.getFileName()); + LocalInfilePacket localInfilePacket = (LocalInfilePacket) resultPacket; + if (log.isLoggable(Level.FINEST)) log.finest("sending local file " + localInfilePacket.getFileName()); String localInfile = localInfilePacket.getFileName(); try { - URL u = new URL(localInfile); - is = u.openStream(); - } catch (IOException ioe) { - is = new FileInputStream(localInfile); + URL u = new URL(localInfile); + is = u.openStream(); + } catch (IOException ioe) { + is = new FileInputStream(localInfile); } } else { - is = localInfileInputStream; - localInfileInputStream = null; + is = localInfileInputStream; + localInfileInputStream = null; } - writer.sendFile(is, rawPacket.getPacketSeq()+1); + writer.sendFile(is, rawPacket.getPacketSeq() + 1); is.close(); rawPacket = packetFetcher.getRawPacket(); resultPacket = ResultPacketFactory.createResultPacket(rawPacket); } } catch (SocketTimeoutException ste) { - this.close(); - throw new QueryException("Could not read resultset: " + ste.getMessage(), + this.close(); + throw new QueryException("Could not read resultset: " + ste.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), ste); - } - catch (IOException e) { + } catch (IOException e) { throw new QueryException("Could not read resultset: " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), @@ -992,7 +981,7 @@ public QueryResult getResult(List dQueries, boolean streaming) throws Que } else { log.warning("Got error from server: " + ((ErrorPacket) resultPacket).getMessage()); } - throw new QueryException(ep.getMessage(), ep.getErrorNumber(), ep.getSqlState()); + throw new QueryException(ep.getMessage(), ep.getErrorNumber(), ep.getSqlState()); case OK: final OKPacket okpacket = (OKPacket) resultPacket; @@ -1003,12 +992,11 @@ public QueryResult getResult(List dQueries, boolean streaming) throws Que okpacket.getWarnings(), okpacket.getMessage(), okpacket.getInsertId()); - log.fine("OK, " + okpacket.getAffectedRows()); + if (log.isLoggable(Level.FINEST)) log.finest("OK, " + okpacket.getAffectedRows()); return updateResult; case RESULTSET: this.hasWarnings = false; - log.fine("SELECT executed, fetching result set"); - ResultSetPacket resultSetPacket = (ResultSetPacket)resultPacket; + ResultSetPacket resultSetPacket = (ResultSetPacket) resultPacket; try { return this.createQueryResult(resultSetPacket, streaming); } catch (IOException e) { @@ -1022,20 +1010,21 @@ public QueryResult getResult(List dQueries, boolean streaming) throws Que log.severe("Could not parse result..." + resultPacket.getResultType()); throw new QueryException("Could not parse result", (short) -1, SQLExceptionMapper.SQLStates.INTERRUPTED_EXCEPTION.getSqlState()); } + } - public QueryResult executeQuery(final Query query, boolean streaming) throws QueryException, SQLException { + @Override + public QueryResult executeQuery(final Query query, boolean streaming) throws QueryException { List queries = new ArrayList(); queries.add(query); return executeQuery(queries, streaming, false, 0); } - public QueryResult executeQuery(final List dQueries, boolean streaming, boolean isRewritable, int rewriteOffset) throws QueryException, SQLException { + public QueryResult executeQuery(final List dQueries, boolean streaming, boolean isRewritable, int rewriteOffset) throws QueryException { for (Query query : dQueries) query.validate(); this.moreResults = false; final StreamedQueryPacket packet = new StreamedQueryPacket(dQueries, isRewritable, rewriteOffset); - try { packet.send(writer); } catch (MaxAllowedPacketException e) { @@ -1044,14 +1033,12 @@ public QueryResult executeQuery(final List dQueries, boolean streaming, b } catch (IOException e) { throw new QueryException("Could not send query: " + e.getMessage(), -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), e); } - if (!isMasterConnection()) - queriesSinceFailover++; + try { return getResult(dQueries, streaming); } catch (QueryException qex) { if (qex.getCause() instanceof SocketTimeoutException) { - close(); - throw SQLExceptionMapper.getSQLException("Connection timed out"); + throw new QueryException("Connection timed out", -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState(), qex); } else { throw qex; } @@ -1059,7 +1046,9 @@ public QueryResult executeQuery(final List dQueries, boolean streaming, b } - public String getServerVariable(String variable) throws QueryException, SQLException { + + + private String getServerVariable(String variable) throws QueryException { CachedSelectResult qr = (CachedSelectResult) executeQuery(new MySQLQuery("select @@" + variable)); try { if (!qr.next()) { @@ -1070,7 +1059,6 @@ public String getServerVariable(String variable) throws QueryException, SQLExcep throw new QueryException(ioe.getMessage(), 0, "HYOOO", ioe); } - try { String value = qr.getValueObject(0).getString(); return value; @@ -1082,27 +1070,21 @@ public String getServerVariable(String variable) throws QueryException, SQLExcep /** * cancels the current query - clones the current protocol and executes a query using the new connection - * - * thread safe * - * @throws QueryException - * @throws SQLException + * @throws QueryException never thrown + * @throws IOException if Host is not responding */ - public void cancelCurrentQuery() throws QueryException, IOException, SQLException { - MySQLProtocol copiedProtocol = new MySQLProtocol(jdbcUrl, username, password, info); + @Override + public void cancelCurrentQuery() throws QueryException, IOException { + MySQLProtocol copiedProtocol = new MySQLProtocol(jdbcUrl, null); + copiedProtocol.setHostAddress(getHostAddress()); + copiedProtocol.connect(); + //no lock, because there is already a query running that possessed the lock. copiedProtocol.executeQuery(new MySQLQuery("KILL QUERY " + serverThreadId)); copiedProtocol.close(); } - public boolean createDB() { - String alias = info.getProperty("createDatabaseIfNotExist"); - return info != null - && (info.getProperty("createDB", "").equalsIgnoreCase("true") - || (alias != null && alias.equalsIgnoreCase("true"))); - } - - - + @Override public QueryResult getMoreResults(boolean streaming) throws QueryException { if(!moreResults) return null; @@ -1129,11 +1111,19 @@ public static String hexdump(ByteBuffer bb, int offset) { } + @Override public boolean hasUnreadData() { - return (activeResult != null); + lock.readLock().lock(); + try { + return (activeResult != null); + } finally { + lock.readLock().unlock(); + } + } - public void setMaxRows(int max) throws QueryException, SQLException{ + @Override + public void setMaxRows(int max) throws QueryException { if (maxRows != max) { if (max == 0) { executeQuery(new MySQLQuery("set @@SQL_SELECT_LIMIT=DEFAULT")); @@ -1143,77 +1133,167 @@ public void setMaxRows(int max) throws QueryException, SQLException{ maxRows = max; } } - + public void setInternalMaxRows(int max) { + if (maxRows != max) { + maxRows = max; + } + } + + public int getMaxRows() { + return maxRows; + } + void parseVersion() { - String[] a = version.split("[^0-9]"); - if (a.length > 0) - majorVersion = Integer.parseInt(a[0]); - if (a.length > 1) - minorVersion = Integer.parseInt(a[1]); - if (a.length > 2) - patchVersion = Integer.parseInt(a[2]); - } - + String[] a = version.split("[^0-9]"); + if (a.length > 0) + majorVersion = Integer.parseInt(a[0]); + if (a.length > 1) + minorVersion = Integer.parseInt(a[1]); + if (a.length > 2) + patchVersion = Integer.parseInt(a[2]); + } + + @Override public int getMajorServerVersion() { - return majorVersion; - + return majorVersion; + } + @Override public int getMinorServerVersion() { - return minorVersion; + return minorVersion; } - + + @Override public boolean versionGreaterOrEqual(int major, int minor, int patch) { - if (this.majorVersion > major) - return true; - if (this.majorVersion < major) - return false; + if (this.majorVersion > major) + return true; + if (this.majorVersion < major) + return false; /* * Major versions are equal, compare minor versions */ - if (this.minorVersion > minor) - return true; - if (this.minorVersion < minor) - return false; - + if (this.minorVersion > minor) + return true; + if (this.minorVersion < minor) + return false; + /* * Minor versions are equal, compare patch version */ - if (this.patchVersion > patch) - return true; - if (this.patchVersion < patch) - return false; - + if (this.patchVersion > patch) + return true; + if (this.patchVersion < patch) + return false; + /* Patch versions are equal => versions are equal */ - return true; + return true; + } + @Override + public void setLocalInfileInputStream(InputStream inputStream) { + this.localInfileInputStream = inputStream; + } + + public int getMaxAllowedPacket() { + return this.maxAllowedPacket; } - public void setLocalInfileInputStream(InputStream inputStream) { - this.localInfileInputStream = inputStream; - } - public void setMaxAllowedPacket(int maxAllowedPacket) { + this.maxAllowedPacket = maxAllowedPacket; writer.setMaxAllowedPacket(maxAllowedPacket); } - + /** * Sets the connection timeout. * @param timeout the timeout, in milliseconds - * @throws SocketException + * @throws SocketException if there is an error in the underlying protocol, such as a TCP error. */ - public void setTimeout(int timeout) throws SocketException { - this.socket.setSoTimeout(timeout); - } - /** - * Returns the connection timeout in milliseconds. - * @return - * @throws SocketException - */ - public int getTimeout() throws SocketException { - return this.socket.getSoTimeout(); - } + @Override + public void setTimeout(int timeout) throws SocketException { + lock.writeLock().lock(); + try { + this.getOptions().socketTimeout = timeout; + this.socket.setSoTimeout(timeout); + } finally { + lock.writeLock().unlock(); + } + } + /** + * Returns the connection timeout in milliseconds. + * @return the connection timeout in milliseconds. + * @throws SocketException if there is an error in the underlying protocol, such as a TCP error. + */ + @Override + public int getTimeout() throws SocketException { + return this.socket.getSoTimeout(); + } + + @Override + public boolean getPinGlobalTxToPhysicalConnection() { + return this.jdbcUrl.getOptions().pinGlobalTxToPhysicalConnection; + } + + + public void setTransactionIsolation(final int level) throws QueryException { + lock.writeLock().lock(); + try { + String query = "SET SESSION TRANSACTION ISOLATION LEVEL"; + switch (level) { + case Connection.TRANSACTION_READ_UNCOMMITTED: + query += " READ UNCOMMITTED"; + break; + case Connection.TRANSACTION_READ_COMMITTED: + query += " READ COMMITTED"; + break; + case Connection.TRANSACTION_REPEATABLE_READ: + query += " REPEATABLE READ"; + break; + case Connection.TRANSACTION_SERIALIZABLE: + query += " SERIALIZABLE"; + break; + default: + throw new QueryException("Unsupported transaction isolation level"); + } + executeQuery(new MySQLQuery(query)); + transactionIsolationLevel = level; + } finally { + lock.writeLock().unlock(); + } + } + public int getTransactionIsolationLevel() { + return transactionIsolationLevel; + } + + public boolean hasWarnings() { + lock.readLock().lock(); + try { + return hasWarnings; + } finally { + lock.readLock().unlock(); + } + } + + public boolean isConnected() { + lock.readLock().lock(); + try { + return connected; + } finally { + lock.readLock().unlock(); + } + } + + public long getServerThreadId(){ + return serverThreadId; + } + public int getDatatypeMappingFlags() { + return datatypeMappingFlags; + } + + public void closeIfActiveResult() { + if (activeResult != null) activeResult.close(); + } + + public boolean isExplicitClosed() { + return explicitClosed; + } - public String getPinGlobalTxToPhysicalConnection() { - return this.info.getProperty("pinGlobalTxToPhysicalConnection", "false"); - } - } 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..4bc2af1d5 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/Protocol.java @@ -0,0 +1,158 @@ +/* +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.Options; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.query.Query; +import org.mariadb.jdbc.internal.common.queryresults.QueryResult; + +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketException; +import java.util.List; + +public interface Protocol { + MySQLProtocol.PrepareResult prepare(String sql) throws QueryException; + + void closePreparedStatement(int statementId) throws QueryException; + + boolean getAutocommit(); + + boolean noBackslashEscapes() throws QueryException; + + void connect() throws QueryException; + + JDBCUrl getJdbcUrl(); + boolean inTransaction(); + void setProxy(FailoverProxy proxy); + FailoverProxy getProxy(); + void setHostAddress(HostAddress hostAddress); + + Options getOptions(); + + boolean hasMoreResults(); + + void close(); + void closeExplicit(); + + boolean isClosed(); + + void setCatalog(String database) throws QueryException; + + String getServerVersion(); + + void setReadonly(boolean readOnly) throws QueryException; + boolean isConnected(); + boolean getReadonly(); + boolean isMasterConnection(); + boolean mustBeMasterConnection(); + HostAddress getHostAddress(); + String getHost(); + + int getPort(); + void rollback(); + String getDatabase(); + + String getUsername(); + + String getPassword(); + + boolean ping() throws QueryException; + + QueryResult executeQuery(Query dQuery) throws QueryException; + QueryResult executeQuery(final List dQueries, boolean streaming, boolean isRewritable, int rewriteOffset) throws QueryException; + QueryResult getResult(List dQuery, boolean streaming) throws QueryException; + + QueryResult executeQuery(Query dQuery, boolean streaming) throws QueryException; + + void cancelCurrentQuery() throws QueryException, IOException; + + QueryResult getMoreResults(boolean streaming) throws QueryException; + + boolean hasUnreadData(); + boolean checkIfMaster() throws QueryException ; + boolean hasWarnings(); + int getDatatypeMappingFlags(); + + + void setMaxRows(int max) throws QueryException; + void setInternalMaxRows(int max); + 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; + + boolean getPinGlobalTxToPhysicalConnection(); + long getServerThreadId(); + void setTransactionIsolation(int level) throws QueryException; + int getTransactionIsolationLevel(); + boolean isExplicitClosed(); + void closeIfActiveResult(); + + void connectWithoutProxy() throws QueryException ; + boolean shouldReconnectWithoutProxy(); + void setHostFailedWithoutProxy(); + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersListener.java new file mode 100644 index 000000000..952bbeb49 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersListener.java @@ -0,0 +1,359 @@ +package org.mariadb.jdbc.internal.mysql.listener; + +/* +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. +*/ + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.common.UrlHAMode; +import org.mariadb.jdbc.internal.common.query.MySQLQuery; +import org.mariadb.jdbc.internal.common.query.Query; +import org.mariadb.jdbc.internal.mysql.FailoverProxy; +import org.mariadb.jdbc.internal.mysql.HandleErrorResult; +import org.mariadb.jdbc.internal.mysql.Protocol; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.*; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +public abstract class AbstractMastersListener implements Listener { + private final static Logger log = Logger.getLogger(AbstractMastersListener.class.getName()); + + /* =========================== Failover variables ========================================= */ + public final JDBCUrl jdbcUrl; + private AtomicLong masterHostFailTimestamp = new AtomicLong(); + + protected AtomicInteger currentConnectionAttempts = new AtomicInteger(); + protected AtomicBoolean currentReadOnlyAsked=new AtomicBoolean(); + private AtomicBoolean masterHostFail = new AtomicBoolean(); + protected AtomicBoolean isLooping = new AtomicBoolean(); + protected ScheduledFuture scheduledFailover = null; + + protected Protocol currentProtocol = null; + + protected FailoverProxy proxy; + + protected long lastRetry = 0; + protected boolean explicitClosed = false; + + protected ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + + protected AbstractMastersListener(JDBCUrl jdbcUrl) { + this.jdbcUrl = jdbcUrl; + this.masterHostFail.set(true); + } + + /** + * list the recent failedConnection + */ + protected static Map blacklist = new HashMap<>(); + + public void setProxy(FailoverProxy proxy) { + this.proxy = proxy; + } + public FailoverProxy getProxy() { return this.proxy; } + + public Map getBlacklist() { + return blacklist; + } + + public HandleErrorResult handleFailover(Method method, Object[] args) throws Throwable { + if (explicitClosed) throw new QueryException("Connection has been closed !"); + if (setMasterHostFail()) { + log.warning("SQL Primary node [" + this.currentProtocol.getHostAddress().toString() + "] connection fail "); + addToBlacklist(currentProtocol.getHostAddress()); + } + return primaryFail(method, args); + } + + /** + * After a failover, put the hostAddress in a static list so the other connection will not take this host in account for a time + * @param hostAddress the HostAddress to add to blacklist + */ + public void addToBlacklist(HostAddress hostAddress) { + if (hostAddress != null) { + if (log.isLoggable(Level.FINE))log.finest("host " + hostAddress+" added to blacklist"); + blacklist.put(hostAddress, System.currentTimeMillis()); + } + } + + /** + * Permit to remove Host to blacklist after loadBalanceBlacklistTimeout seconds + */ + public void resetOldsBlackListHosts() { + long currentTime = System.currentTimeMillis(); + Set currentBlackListkeys = new HashSet(blacklist.keySet()); + for (HostAddress blackListHost : currentBlackListkeys) { + if (blacklist.get(blackListHost) < currentTime - jdbcUrl.getOptions().loadBalanceBlacklistTimeout * 1000) { + if (log.isLoggable(Level.FINE)) log.finest("host " + blackListHost+" remove of blacklist"); + blacklist.remove(blackListHost); + } + } + } + + protected void resetMasterFailoverData() { + if (masterHostFail.compareAndSet(true, false)) masterHostFailTimestamp.set(0); + } + + /** + * private class to permit a timer reconnection loop + */ + protected class FailLoop implements Runnable { + Listener listener; + public FailLoop(Listener listener) { + log.finest("launched FailLoop"); + this.listener = listener; + } + + public void run() { + if (hasHostFail()) { + if (log.isLoggable(Level.FINEST)) log.finest("failLoop , listener.shouldReconnect() : "+listener.shouldReconnect()); + if (listener.shouldReconnect()) { + try { + if (currentConnectionAttempts.get() >= jdbcUrl.getOptions().failoverLoopRetries) + throw new QueryException("Too many reconnection attempts (" + jdbcUrl.getOptions().retriesAllDown + ")"); + SearchFilter filter = getFilterForFailedHost(); + filter.setUniqueLoop(true); + listener.reconnectFailedConnection(filter); + //reconnection done ! + stopFailover(); + } catch (Exception e) { + log.log(Level.FINEST, "FailLoop search connection failed", e); + } + } else { + if (currentConnectionAttempts.get() > jdbcUrl.getOptions().retriesAllDown) { + log.fine("stopping failover after too many attemps ("+currentConnectionAttempts+")"); + stopFailover(); + } + } + } else { + stopFailover(); + } + log.finest("end launched FailLoop"); + } + } + + protected void setSessionReadOnly(boolean readOnly) throws QueryException { + if (this.currentProtocol.versionGreaterOrEqual(10, 0, 0)) { + this.currentProtocol.executeQuery(new MySQLQuery("SET SESSION TRANSACTION "+(readOnly?"READ ONLY":"READ WRITE"))); + } + } + + protected void stopFailover() { + if (isLooping.compareAndSet(true, false)) { + log.finest("stopping failover"); + if (scheduledFailover!=null)scheduledFailover.cancel(false); + } + } + + /** + * launch the scheduler loop every 250 milliseconds, to reconnect a failed connection. + * Will verify if there is an existing scheduler + * @param now now will launch the loop immediatly, 250ms after if false + */ + protected void launchFailLoopIfNotlaunched(boolean now) { + if (isLooping.compareAndSet(false, true) && jdbcUrl.getOptions().failoverLoopRetries != 0) { + scheduledFailover = Executors.newSingleThreadScheduledExecutor().scheduleWithFixedDelay(new FailLoop(this), now ? 0 : 250, 250, TimeUnit.MILLISECONDS); + } + } + + public Protocol getCurrentProtocol() { + return currentProtocol; + } + + public long getMasterHostFailTimestamp() { + return masterHostFailTimestamp.get(); + } + + public boolean setMasterHostFail() { + if (masterHostFail.compareAndSet(false, true)) { + masterHostFailTimestamp.set(System.currentTimeMillis()); + currentConnectionAttempts.set(0); + return true; + } + return false; + } + + public boolean isMasterHostFail() { + return masterHostFail.get(); + } + + public boolean hasHostFail() { + return masterHostFail.get(); + } + + public SearchFilter getFilterForFailedHost() { + return new SearchFilter(isMasterHostFail(), false); + } + + /** + * After a failover that has bean done, relaunche the operation that was in progress. + * In case of special operation that crash serveur, doesn't relaunched it; + * @param method the methode accessed + * @param args the parameters + * @return An object that indicate the result or that the exception as to be thrown + * @throws IllegalAccessException if the initial call is not permit + * @throws InvocationTargetException if there is any error relaunching initial method + */ + public HandleErrorResult relaunchOperation(Method method, Object[] args) throws IllegalAccessException, InvocationTargetException{ + HandleErrorResult handleErrorResult = new HandleErrorResult(true); + if (method != null) { + if ("executeQuery".equals(method.getName())) { + String query = ((Query)args[0]).getQuery().toUpperCase(); + if (!query.equals("ALTER SYSTEM CRASH") + && !query.startsWith("KILL")) { + handleErrorResult.resultObject = method.invoke(currentProtocol, args); + handleErrorResult.mustThrowError = false; + } + } else { + handleErrorResult.resultObject = method.invoke(currentProtocol, args); + handleErrorResult.mustThrowError = false; + } + } + return handleErrorResult; + } + + public Object invoke(Method method, Object[] args) throws Throwable { + return method.invoke(currentProtocol, args); + } + + + /** + * when switching between 2 connections, report existing connection parameter to the new used connection + * @param from used connection + * @param to will-be-current connection + * @throws QueryException if catalog cannot be set + */ + public void syncConnection(Protocol from, Protocol to) throws QueryException { + + if (from != null) { + proxy.lock.writeLock().lock(); + + try { + to.setMaxAllowedPacket(from.getMaxAllowedPacket()); + to.setMaxRows(from.getMaxRows()); + to.setInternalMaxRows(from.getMaxRows()); + if (from.getTransactionIsolationLevel() != 0) { + to.setTransactionIsolation(from.getTransactionIsolationLevel()); + } + if (from.getDatabase() != null && !"".equals(from.getDatabase()) && !from.getDatabase().equals(to.getDatabase())) { + to.setCatalog(from.getDatabase()); + } + if (from.getAutocommit() != to.getAutocommit()) { + to.executeQuery(new MySQLQuery("set autocommit=" + (from.getAutocommit() ? "1" : "0"))); + } + } finally { + proxy.lock.writeLock().unlock(); + } + + } + } + public boolean isClosed() { + return currentProtocol.isClosed(); + } + + public boolean isReadOnly() { + return currentReadOnlyAsked.get(); + } + + public boolean isExplicitClosed() { + return explicitClosed; + } + + public void setExplicitClosed(boolean explicitClosed) { + this.explicitClosed = explicitClosed; + } + + public int getRetriesAllDown() { + return jdbcUrl.getOptions().retriesAllDown; + } + + public int getInitialTimeout() { + return jdbcUrl.getOptions().initialTimeout; + } + + public boolean isAutoReconnect() { + return jdbcUrl.getOptions().autoReconnect; + } + + public JDBCUrl getJdbcUrl() { + return jdbcUrl; + } + + public abstract void initializeConnection() throws QueryException; + + public abstract void preExecute() throws QueryException; + + public abstract void preClose() throws SQLException; + + public abstract boolean shouldReconnect() ; + + public abstract void reconnectFailedConnection(SearchFilter filter) throws QueryException ; + + public abstract void switchReadOnlyConnection(Boolean readonly) throws QueryException; + + public abstract HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable ; + + public abstract void throwFailoverMessage(QueryException queryException, boolean reconnected) throws QueryException; + + public abstract void reconnect() throws QueryException; + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersSlavesListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersSlavesListener.java new file mode 100644 index 000000000..d0f78a12e --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/AbstractMastersSlavesListener.java @@ -0,0 +1,147 @@ +package org.mariadb.jdbc.internal.mysql.listener; + +/* +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. +*/ + +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.mysql.FailoverProxy; +import org.mariadb.jdbc.internal.mysql.HandleErrorResult; +import org.mariadb.jdbc.internal.mysql.Protocol; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.lang.reflect.Method; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +public abstract class AbstractMastersSlavesListener extends AbstractMastersListener { + private final static Logger log = Logger.getLogger(AbstractMastersSlavesListener.class.getName()); + + /* =========================== Failover variables ========================================= */ + private AtomicLong secondaryHostFailTimestamp = new AtomicLong(); + private AtomicBoolean secondaryHostFail = new AtomicBoolean(); + protected AtomicInteger queriesSinceFailover = new AtomicInteger(); + + protected AbstractMastersSlavesListener(JDBCUrl jdbcUrl) { + super(jdbcUrl); + this.secondaryHostFail.set(true); + } + + public HandleErrorResult handleFailover(Method method, Object[] args) throws Throwable { + if (explicitClosed) throw new QueryException("Connection has been closed !"); + if (currentProtocol.mustBeMasterConnection()) { + if (setMasterHostFail()) { + log.warning("SQL Primary node [" + this.currentProtocol.getHostAddress().toString() + "] connection fail "); + addToBlacklist(currentProtocol.getHostAddress()); + if (FailoverProxy.METHOD_EXECUTE_QUERY.equals(method.getName())) queriesSinceFailover.incrementAndGet(); + } + return primaryFail(method, args); + } else { + if (setSecondaryHostFail()) { + log.warning("SQL Secondary node [" + this.currentProtocol.getHostAddress().toString() + "] connection fail "); + addToBlacklist(currentProtocol.getHostAddress()); + if (FailoverProxy.METHOD_EXECUTE_QUERY.equals(method.getName())) queriesSinceFailover.incrementAndGet(); + } + return secondaryFail(method, args); + } + } + + @Override + protected void resetMasterFailoverData() { + super.resetMasterFailoverData(); + + //if all connection are up, reset failovers timers + if (!secondaryHostFail.get()) { + currentConnectionAttempts.set(0); + lastRetry = 0; + queriesSinceFailover.set(0);; + } + } + + protected void resetSecondaryFailoverData() { + if (secondaryHostFail.compareAndSet(true, false)) secondaryHostFailTimestamp.set(0); + + //if all connection are up, reset failovers timers + if (!isMasterHostFail()) { + currentConnectionAttempts.set(0); + lastRetry = 0; + queriesSinceFailover.set(0); + } + } + + public long getSecondaryHostFailTimestamp() { + return secondaryHostFailTimestamp.get(); + } + + public boolean setSecondaryHostFail() { + if (secondaryHostFail.compareAndSet(false, true)) { + secondaryHostFailTimestamp.set(System.currentTimeMillis()); + currentConnectionAttempts.set(0); + return true; + } + return false; + } + + public boolean isSecondaryHostFail() { + return secondaryHostFail.get(); + } + + public boolean hasHostFail() { + return isMasterHostFail() || isSecondaryHostFail(); + } + public SearchFilter getFilterForFailedHost() { + return new SearchFilter(isMasterHostFail(), isSecondaryHostFail()); + } + + public abstract HandleErrorResult secondaryFail(Method method, Object[] args) throws Throwable; + public abstract void foundActiveSecondary(Protocol newSecondaryProtocol); + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/Listener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/Listener.java new file mode 100644 index 000000000..313bb4f59 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/Listener.java @@ -0,0 +1,89 @@ +/* +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.listener; + +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.common.QueryException; +import org.mariadb.jdbc.internal.mysql.FailoverProxy; +import org.mariadb.jdbc.internal.mysql.HandleErrorResult; +import org.mariadb.jdbc.internal.mysql.Protocol; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.Map; + +public interface Listener { + void setProxy(FailoverProxy proxy); + FailoverProxy getProxy(); + void initializeConnection() throws QueryException; + void preExecute() throws QueryException; + void preClose() throws SQLException; + boolean shouldReconnect(); + void reconnectFailedConnection(SearchFilter filter) throws QueryException; + void switchReadOnlyConnection(Boolean readonly) throws QueryException ; + HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable; + Object invoke(Method method, Object[] args) throws Throwable; + HandleErrorResult handleFailover(Method method, Object[] args) throws Throwable; + void foundActiveMaster(Protocol protocol) throws QueryException; + Map getBlacklist(); + void syncConnection(Protocol from, Protocol to) throws QueryException; + JDBCUrl getJdbcUrl(); + void throwFailoverMessage(QueryException queryException, boolean reconnected) throws QueryException; + int getInitialTimeout(); + boolean isAutoReconnect(); + int getRetriesAllDown(); + boolean isExplicitClosed(); + void setExplicitClosed(boolean explicitClosed); + void reconnect() throws QueryException; + boolean isReadOnly(); + boolean isClosed(); +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/AuroraListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/AuroraListener.java new file mode 100644 index 000000000..38326a443 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/AuroraListener.java @@ -0,0 +1,242 @@ +/* +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.listener.impl; + +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 org.mariadb.jdbc.internal.mysql.*; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class AuroraListener extends MastersSlavesListener { + private final static Logger log = Logger.getLogger(AuroraListener.class.getName()); + + public AuroraListener(JDBCUrl jdbcUrl) { + super(jdbcUrl); + masterProtocol = null; + secondaryProtocol = null; + lastQueryTime = System.currentTimeMillis(); + } + + @Override + public void initializeConnection() throws QueryException { + if (jdbcUrl.getOptions().validConnectionTimeout != 0) + scheduledPing = executorService.scheduleWithFixedDelay(new PingLoop(this), jdbcUrl.getOptions().validConnectionTimeout, jdbcUrl.getOptions().validConnectionTimeout, TimeUnit.SECONDS); + try { + reconnectFailedConnection(new SearchFilter(true, true, true)); + } catch (QueryException e) { + log.log(Level.FINEST, "initializeConnection failed", e); + checkInitialConnection(); + throw e; + } + } + + /** + * search a valid connection for failed one. + * A Node can be a master or a replica depending on the cluster state. + * so search for each host until found all the failed connection. + * By default, search for the host not down, and recheck the down one after if not found valid connections. + * + * @throws QueryException if a connection asked is not found + */ + @Override + public void reconnectFailedConnection(SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINEST)) log.finest("search connection searchFilter=" + searchFilter); + currentConnectionAttempts.incrementAndGet(); + resetOldsBlackListHosts(); + + //put the list in the following order + // - random order not connected host + // - random order blacklist host + // - random order connected host + List loopAddress = new LinkedList<>(jdbcUrl.getHostAddresses()); + loopAddress.removeAll(blacklist.keySet()); + Collections.shuffle(loopAddress); + List blacklistShuffle = new LinkedList<>(blacklist.keySet()); + Collections.shuffle(blacklistShuffle); + loopAddress.addAll(blacklistShuffle); + + //put connected at end + if (masterProtocol != null && !isMasterHostFail()) { + loopAddress.remove(masterProtocol.getHostAddress()); + //loopAddress.add(masterProtocol.getHostAddress()); + } + + if (!isSecondaryHostFail()) { + if (secondaryProtocol != null) { + loopAddress.remove(secondaryProtocol.getHostAddress()); + //loopAddress.add(secondaryProtocol.getHostAddress()); + } + if (isMasterHostFail()) { + log.fine("searching probableMaster"); + HostAddress probableMaster = searchByStartName(secondaryProtocol, loopAddress); + + if (probableMaster != null) { + loopAddress.remove(probableMaster); + loopAddress.add(0, probableMaster); + } else if (log.isLoggable(Level.FINEST)) log.finest("probableMaster not found"); + } + } + + if (((searchFilter.isSearchForMaster() && isMasterHostFail())|| (searchFilter.isSearchForSlave() && isSecondaryHostFail())) || searchFilter.isInitialConnection()) { + AuroraProtocol.loop(this, loopAddress, blacklist, searchFilter); + } + } + + + /** + * Aurora replica doesn't have the master endpoint but the master instance name. + * since the end point normally use the instance name like "instancename.some_ugly_string.region.rds.amazonaws.com", if an endpoint start with this instance name, it will be checked first. + * @return the probable master address or null if not found + * @param secondaryProtocol the current secondary protocol + * @param loopAddress list of possible hosts + */ + public HostAddress searchByStartName(Protocol secondaryProtocol, List loopAddress) { + if (!isSecondaryHostFail()) { + SelectQueryResult queryResult = null; + try { + proxy.lock.writeLock().lock(); + try { + queryResult = (SelectQueryResult) secondaryProtocol.executeQuery(new MySQLQuery("select server_id from information_schema.replica_host_status where session_id = 'MASTER_SESSION_ID'")); + queryResult.next(); + } finally { + proxy.lock.writeLock().unlock(); + } + String masterHostName = queryResult.getValueObject(0).getString(); + for (int i = 0; i < loopAddress.size(); i++) { + if (loopAddress.get(i).host.startsWith(masterHostName)) { + if (log.isLoggable(Level.FINEST)) log.finest("master probably " + loopAddress.get(i)); + return loopAddress.get(i); + } + } + } catch (IOException ioe) { + log.log(Level.FINEST, "searchByStartName failed", ioe); + //eat exception + } catch (QueryException qe) { + if (proxy.hasToHandleFailover(qe)) { + if (setSecondaryHostFail()) { + log.warning("SQL Secondary node [" + this.currentProtocol.getHostAddress().toString() + "] connection fail "); + addToBlacklist(currentProtocol.getHostAddress()); + } + } + } finally { + if (queryResult != null) { + queryResult.close(); + } + } + } + return null; + } + + @Override + public void checkIfTypeHaveChanged(SearchFilter searchFilter) throws QueryException { + if (!isMasterHostFail()) { + try { + if (masterProtocol != null && !masterProtocol.checkIfMaster()) { + //master has been demote, is now secondary + setMasterHostFail(); + if (isSecondaryHostFail()) foundActiveSecondary(masterProtocol); + if (searchFilter != null) searchFilter.setSearchForSlave(false); + launchFailLoopIfNotlaunched(false); + } + } catch (QueryException e) { + try { + masterProtocol.ping(); + } catch (QueryException ee) { + proxy.lock.writeLock().lock(); + try { + masterProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + if (setMasterHostFail()) addToBlacklist(masterProtocol.getHostAddress()); + } + launchFailLoopIfNotlaunched(false); + } + } + + if (!isSecondaryHostFail()) { + try { + if (secondaryProtocol != null && secondaryProtocol.checkIfMaster()) { + //secondary has been promoted to master + setSecondaryHostFail(); + if (isMasterHostFail()) foundActiveMaster(secondaryProtocol); + if (searchFilter != null) searchFilter.setSearchForMaster(false); + launchFailLoopIfNotlaunched(false); + } + } catch (QueryException e) { + try { + this.secondaryProtocol.ping(); + } catch (Exception ee) { + proxy.lock.writeLock().lock(); + try { + secondaryProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + if (setSecondaryHostFail()) addToBlacklist(this.secondaryProtocol.getHostAddress()); + launchFailLoopIfNotlaunched(false); + } + + } + } + } + +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersFailoverListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersFailoverListener.java new file mode 100644 index 000000000..32a7269b9 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersFailoverListener.java @@ -0,0 +1,293 @@ +/* +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.listener.impl; + +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.UrlHAMode; +import org.mariadb.jdbc.internal.mysql.HandleErrorResult; +import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.Protocol; +import org.mariadb.jdbc.internal.mysql.listener.AbstractMastersListener; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + + +public class MastersFailoverListener extends AbstractMastersListener { + private final static Logger log = Logger.getLogger(MastersFailoverListener.class.getName()); + private final UrlHAMode mode; + + public MastersFailoverListener(final JDBCUrl jdbcUrl) { + super(jdbcUrl); + this.mode = jdbcUrl.getHaMode(); + + } + + public void initializeConnection() throws QueryException { + this.currentProtocol = null; + log.finest("launching initial loop"); + reconnectFailedConnection(new SearchFilter(true, false)); + log.finest("launching initial loop end"); + + } + + public void preExecute() throws QueryException { + //if connection is closed or failed on slave + if (this.currentProtocol != null && this.currentProtocol.isClosed()) { + if (!isExplicitClosed() && jdbcUrl.getOptions().autoReconnect) { + try { + reconnectFailedConnection(new SearchFilter(isMasterHostFail(), false, !currentReadOnlyAsked.get(), currentReadOnlyAsked.get())); + } catch (QueryException e) { + } + } else + throw new QueryException("Connection is closed", (short) -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState()); + } + } + + public boolean shouldReconnect() { + return isMasterHostFail(); + } + + @Override + public void preClose() throws SQLException { + setExplicitClosed(true); + proxy.lock.writeLock().lock(); + try { + if (currentProtocol != null && this.currentProtocol.isConnected()) this.currentProtocol.close(); + } finally { + if (!UrlHAMode.NONE.equals(mode)) { + proxy.lock.writeLock().unlock(); + if (scheduledFailover != null) { + scheduledFailover.cancel(true); + isLooping.set(false); + } + executorService.shutdownNow(); + try { + executorService.awaitTermination(15, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.finest("executorService interrupted"); + } + } + } + log.finest("preClose connections"); + } + + @Override + public HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable { + boolean alreadyClosed = !currentProtocol.isConnected(); + try { + if (currentProtocol != null && currentProtocol.isConnected() && currentProtocol.ping()) { + if (log.isLoggable(Level.FINE)) + log.fine("Primary node [" + currentProtocol.getHostAddress().toString() + "] connection re-established"); + + // if in transaction cannot be sure that the last query has been received by server of not, so rollback. + if (currentProtocol.inTransaction()) { + currentProtocol.rollback(); + } + return new HandleErrorResult(true); + } + } catch (QueryException e) { + proxy.lock.writeLock().lock(); + try { + currentProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + if (setMasterHostFail()) addToBlacklist(currentProtocol.getHostAddress()); + } + + try { + reconnectFailedConnection(new SearchFilter(true, false)); + if (!UrlHAMode.NONE.equals(mode)) launchFailLoopIfNotlaunched(true); + if (alreadyClosed) return relaunchOperation(method, args); + return new HandleErrorResult(true); + } catch (Exception e) { + if (!UrlHAMode.NONE.equals(mode)) launchFailLoopIfNotlaunched(true); + return new HandleErrorResult(); + } + } + + /** + * Loop to connect + * + * @throws QueryException if there is any error during reconnection + * @throws QueryException sqlException + */ + @Override + public void reconnectFailedConnection(SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINEST)) log.finest("search connection searchFilter=" + searchFilter); + currentConnectionAttempts.incrementAndGet(); + resetOldsBlackListHosts(); + + List loopAddress = new LinkedList<>(jdbcUrl.getHostAddresses()); + if (UrlHAMode.FAILOVER.equals(mode)) { + //put the list in the following order + // - random order not connected host + // - random order blacklist host + // - random order connected host + loopAddress.removeAll(blacklist.keySet()); + Collections.shuffle(loopAddress); + List blacklistShuffle = new LinkedList<>(blacklist.keySet()); + Collections.shuffle(blacklistShuffle); + loopAddress.addAll(blacklistShuffle); + } else { + //order in sequence + loopAddress.removeAll(blacklist.keySet()); + loopAddress.addAll(blacklist.keySet()); + } + + //put connected at end + if (currentProtocol != null && !isMasterHostFail()) { + loopAddress.remove(currentProtocol.getHostAddress()); + //loopAddress.add(currentProtocol.getHostAddress()); + } + + MySQLProtocol.loop(this, loopAddress, blacklist, searchFilter); + + //if no error, reset failover variables + resetMasterFailoverData(); + } + + + public void switchReadOnlyConnection(Boolean mustBeReadOnly) throws QueryException { + if (currentReadOnlyAsked.compareAndSet(!mustBeReadOnly, mustBeReadOnly)) { + setSessionReadOnly(mustBeReadOnly); + } + } + + /** + * method called when a new Master connection is found after a fallback + * @param protocol the new active connection + */ + @Override + public void foundActiveMaster(Protocol protocol) throws QueryException { + if (isExplicitClosed()) { + proxy.lock.writeLock().lock(); + try { + protocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + return; + } + syncConnection(this.currentProtocol, protocol); + proxy.lock.writeLock().lock(); + try { + if (currentProtocol != null && !currentProtocol.isClosed()) currentProtocol.close(); + currentProtocol = protocol; + } finally { + proxy.lock.writeLock().unlock(); + } + + if (currentReadOnlyAsked.get()) { + setSessionReadOnly(true); + } + + if (log.isLoggable(Level.FINE)) { + if (getMasterHostFailTimestamp() > 0) { + log.fine("new primary node [" + currentProtocol.getHostAddress().toString() + "] connection established after " + (System.currentTimeMillis() - getMasterHostFailTimestamp())); + } else log.fine("new primary node [" + currentProtocol.getHostAddress().toString() + "] connection established"); + } + + resetMasterFailoverData(); + stopFailover(); + } + + + /** + * Throw a human readable message after a failoverException + * + * @param queryException internal error + * @param reconnected connection status + * @throws QueryException error with failover information + */ + @Override + public void throwFailoverMessage(QueryException queryException, boolean reconnected) throws QueryException { + HostAddress hostAddress = (currentProtocol != null) ? currentProtocol.getHostAddress() : null; + + String firstPart = "Communications link failure with primary" + ((hostAddress != null) ? " host " + hostAddress.host + ":" + hostAddress.port : "") + ". "; + String error = ""; + if (jdbcUrl.getOptions().autoReconnect) { + if (isMasterHostFail()) + error += " Driver will reconnect automatically in a few millisecond or during next query if append before"; + else error += " Driver as successfully reconnect connection"; + } else { + if (reconnected) { + error += " Driver as reconnect connection"; + } else { + if (shouldReconnect()) { + error += " Driver will try to reconnect automatically in a few millisecond or during next query if append before"; + } + } + } + if (queryException == null) { + throw new QueryException(firstPart + error, (short) -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState()); + } else { + error = queryException.getMessage() + ". " + error; + queryException.setMessage(firstPart + error); + throw queryException; + } + } + + + public void reconnect() throws QueryException { + reconnectFailedConnection(new SearchFilter(true, false)); + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersSlavesListener.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersSlavesListener.java new file mode 100644 index 000000000..3e9b245c0 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/impl/MastersSlavesListener.java @@ -0,0 +1,685 @@ +/* +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.listener.impl; + +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.mysql.HandleErrorResult; +import org.mariadb.jdbc.internal.mysql.Protocol; +import org.mariadb.jdbc.internal.mysql.MastersSlavesProtocol; +import org.mariadb.jdbc.internal.mysql.listener.AbstractMastersSlavesListener; +import org.mariadb.jdbc.internal.mysql.listener.tools.SearchFilter; + +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * this class handle the operation when multiple hosts. + */ +public class MastersSlavesListener extends AbstractMastersSlavesListener { + private final static Logger log = Logger.getLogger(MastersSlavesListener.class.getName()); + + protected Protocol masterProtocol; + protected Protocol secondaryProtocol; + protected long lastQueryTime = 0; + protected ScheduledFuture scheduledPing = null; + + public MastersSlavesListener(final JDBCUrl jdbcUrl) { + super(jdbcUrl); + masterProtocol = null; + secondaryProtocol = null; + lastQueryTime = System.currentTimeMillis(); + + } + + public void initializeConnection() throws QueryException { + if (jdbcUrl.getOptions().validConnectionTimeout != 0) + scheduledPing = executorService.scheduleWithFixedDelay(new PingLoop(this), jdbcUrl.getOptions().validConnectionTimeout, jdbcUrl.getOptions().validConnectionTimeout, TimeUnit.SECONDS); + try { + reconnectFailedConnection(new SearchFilter(true, true, true)); + } catch (QueryException e) { + log.log(Level.FINEST, "initializeConnection failed", e); + checkInitialConnection(); + throwFailoverMessage(e, false); + } + } + + protected void checkInitialConnection() { + if (this.masterProtocol != null && !this.masterProtocol.isConnected()) { + setMasterHostFail(); + } + if (this.secondaryProtocol != null && !this.secondaryProtocol.isConnected()) { + setSecondaryHostFail(); + } + launchFailLoopIfNotlaunched(false); + } + + public void preClose() throws SQLException { + setExplicitClosed(true); + log.finest("preClose connections"); + proxy.lock.writeLock().lock(); + try { + if (masterProtocol != null && this.masterProtocol.isConnected()) this.masterProtocol.close(); + if (secondaryProtocol != null && this.secondaryProtocol.isConnected()) this.secondaryProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + if (scheduledPing != null) scheduledPing.cancel(true); + + if (scheduledFailover != null) { + scheduledFailover.cancel(true); + isLooping.set(false); + } + executorService.shutdownNow(); + try { + executorService.awaitTermination(15, TimeUnit.SECONDS); + } catch (InterruptedException e) { + log.finest("executorService interrupted"); + } + } + log.finest("preClose connections end"); + } + + @Override + public void preExecute() throws QueryException { + //if connection is closed or failed on slave + if (this.currentProtocol != null && + (this.currentProtocol.isClosed() || + + (!currentReadOnlyAsked.get() && !currentProtocol.isMasterConnection()))) { + queriesSinceFailover.incrementAndGet(); + if (!isExplicitClosed() && jdbcUrl.getOptions().autoReconnect) { + try { + reconnectFailedConnection(new SearchFilter(isMasterHostFail(), isSecondaryHostFail(), !currentReadOnlyAsked.get(), currentReadOnlyAsked.get())); + } catch (QueryException e) { + } + } else + throw new QueryException("Connection is closed", (short) -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState()); + } + if (isMasterHostFail() || isSecondaryHostFail()) { + queriesSinceFailover.incrementAndGet(); + } + + if (jdbcUrl.getOptions().validConnectionTimeout != 0) lastQueryTime = System.currentTimeMillis(); + } + + + /** + * When failing to a different type of host, when to retry + * So he doesn't appear here. + * + * @return true if should reconnect. + */ + public boolean shouldReconnect() { + if (isMasterHostFail() || isSecondaryHostFail()) { + if (currentConnectionAttempts.get() > jdbcUrl.getOptions().retriesAllDown) return false; + long now = System.currentTimeMillis(); + + if (isMasterHostFail()) { + if (jdbcUrl.getOptions().queriesBeforeRetryMaster > 0 && queriesSinceFailover.get() >= jdbcUrl.getOptions().queriesBeforeRetryMaster) + return true; + if (jdbcUrl.getOptions().secondsBeforeRetryMaster > 0 && (now - getMasterHostFailTimestamp()) >= jdbcUrl.getOptions().secondsBeforeRetryMaster * 1000) + return true; + } + + if (isSecondaryHostFail()) { + if (jdbcUrl.getOptions().secondsBeforeRetryMaster > 0 && (now - getSecondaryHostFailTimestamp()) >= jdbcUrl.getOptions().secondsBeforeRetryMaster * 1000) + return true; + } + } + return false; + } + + + /** + * Loop to connect + * + * @throws QueryException if there is any error during reconnection + * @throws QueryException sqlException + */ + public void reconnectFailedConnection(SearchFilter searchFilter) throws QueryException { + if (log.isLoggable(Level.FINEST)) log.fine("search connection searchFilter=" + searchFilter); + currentConnectionAttempts.incrementAndGet(); + resetOldsBlackListHosts(); + + //put the list in the following order + // - random order not connected host + // - random order blacklist host + // - random order connected host + List loopAddress = new LinkedList<>(jdbcUrl.getHostAddresses()); + loopAddress.removeAll(blacklist.keySet()); + Collections.shuffle(loopAddress); + List blacklistShuffle = new LinkedList<>(blacklist.keySet()); + Collections.shuffle(blacklistShuffle); + loopAddress.addAll(blacklistShuffle); + + //put connected at end + if (masterProtocol != null && !isMasterHostFail()) { + loopAddress.remove(masterProtocol.getHostAddress()); + //loopAddress.add(masterProtocol.getHostAddress()); + } + + if (secondaryProtocol != null && !isSecondaryHostFail()) { + loopAddress.remove(secondaryProtocol.getHostAddress()); + //loopAddress.add(secondaryProtocol.getHostAddress()); + } + + if (((searchFilter.isSearchForMaster() && isMasterHostFail()) || (searchFilter.isSearchForSlave() && isSecondaryHostFail())) || searchFilter.isInitialConnection()) { + MastersSlavesProtocol.loop(this, loopAddress, blacklist, searchFilter); + } + } + + /** + * method called when a new Master connection is found after a fallback + * + * @param newMasterProtocol the new active connection + */ + public void foundActiveMaster(Protocol newMasterProtocol) { + if (isExplicitClosed()) { + newMasterProtocol.close(); + return; + } + log.finest("log $$1 " + proxy.lock.getReadLockCount() + " " + proxy.lock.getWriteHoldCount()); + + proxy.lock.writeLock().lock(); + try { + log.finest("log $$2 " + proxy.lock.getReadLockCount()+ " " + proxy.lock.getWriteHoldCount()); + if (masterProtocol != null && !masterProtocol.isClosed()) masterProtocol.close(); + this.masterProtocol = (MastersSlavesProtocol) newMasterProtocol; + if (!currentReadOnlyAsked.get() || isSecondaryHostFail()) { + //actually on a secondary read-only because master was unknown. + //So select master as currentConnection + try { + syncConnection(currentProtocol, this.masterProtocol); + } catch (Exception e) { + log.log(Level.FINE, "Some error append during connection parameter synchronisation : ", e); + } + log.finest("switching current connection to master connection"); + currentProtocol = this.masterProtocol; + } + + if (log.isLoggable(Level.FINE)) { + if (getMasterHostFailTimestamp() > 0) { + log.fine("new primary node [" + newMasterProtocol.getHostAddress().toString() + "] connection established after " + (System.currentTimeMillis() - getMasterHostFailTimestamp())); + } else + log.fine("new primary node [" + newMasterProtocol.getHostAddress().toString() + "] connection established"); + } + resetMasterFailoverData(); + if (!isSecondaryHostFail()) stopFailover(); + } finally { + proxy.lock.writeLock().unlock(); + log.finest("log $$3 " + proxy.lock.getReadLockCount()+ " " + proxy.lock.getWriteHoldCount()); + + } + + } + + + /** + * method called when a new secondary connection is found after a fallback + * + * @param newSecondaryProtocol the new active connection + */ + public void foundActiveSecondary(Protocol newSecondaryProtocol) { + if (isExplicitClosed()) { + newSecondaryProtocol.close(); + return; + } + + proxy.lock.writeLock().lock(); + try { + if (secondaryProtocol != null && !secondaryProtocol.isClosed()) secondaryProtocol.close(); + + log.finest("found active secondary connection"); + this.secondaryProtocol = newSecondaryProtocol; + + //if asked to be on read only connection, switching to this new connection + if (currentReadOnlyAsked.get() || (jdbcUrl.getOptions().failOnReadOnly && !currentReadOnlyAsked.get() && isMasterHostFail())) { + try { + syncConnection(currentProtocol, this.secondaryProtocol); + } catch (Exception e) { + log.log(Level.FINE, "Some error append during connection parameter synchronisation : ", e); + } + currentProtocol = this.secondaryProtocol; + } + + if (log.isLoggable(Level.FINE)) { + if (getSecondaryHostFailTimestamp() > 0) { + log.fine("new active secondary node [" + newSecondaryProtocol.getHostAddress().toString() + "] connection established after " + (System.currentTimeMillis() - getSecondaryHostFailTimestamp())); + } else + log.fine("new active secondary node [" + newSecondaryProtocol.getHostAddress().toString() + "] connection established"); + + } + resetSecondaryFailoverData(); + if (!isMasterHostFail()) stopFailover(); + } finally { + proxy.lock.writeLock().unlock(); + } + } + + /** + * switch to a read-only(secondary) or read and write connection(master) + * + * @param mustBeReadOnly the read-only status asked + * @throws QueryException if operation hasn't change protocol + */ + @Override + public void switchReadOnlyConnection(Boolean mustBeReadOnly) throws QueryException { + if (log.isLoggable(Level.FINEST)) log.fine("switching to mustBeReadOnly = " + mustBeReadOnly + " mode"); + + if (mustBeReadOnly != currentReadOnlyAsked.get() && currentProtocol.inTransaction()) { + throw new QueryException("Trying to set to read-only mode during a transaction"); + } + if (currentReadOnlyAsked.compareAndSet(!mustBeReadOnly, mustBeReadOnly)) { + if (currentReadOnlyAsked.get()) { + if (currentProtocol.isMasterConnection()) { + //must change to replica connection + if (!isSecondaryHostFail()) { + proxy.lock.writeLock().lock(); + try { + + log.finest("switching to secondary connection"); + syncConnection(this.masterProtocol, this.secondaryProtocol); + + currentProtocol = this.secondaryProtocol; + setSessionReadOnly(true); + + log.finest("current connection is now secondary"); + return; + } catch (QueryException e) { + log.log(Level.FINEST, "switching to secondary connection failed", e); + if (setSecondaryHostFail()) { + addToBlacklist(secondaryProtocol.getHostAddress()); + } + } finally { + proxy.lock.writeLock().unlock(); + } + } + launchFailLoopIfNotlaunched(false); + throwFailoverMessage(new QueryException("master " + masterProtocol.getHostAddress() + " connection failed"), false); + } + } else { + if (!currentProtocol.isMasterConnection()) { + //must change to master connection + if (!isMasterHostFail()) { + + proxy.lock.writeLock().lock(); + try { + log.finest("switching to master connection"); + + syncConnection(this.secondaryProtocol, this.masterProtocol); + currentProtocol = this.masterProtocol; + + log.fine("current connection is now master"); + return; + } catch (QueryException e) { + log.log(Level.FINE, "switching to master connection failed", e); + if (setMasterHostFail()) { + addToBlacklist(masterProtocol.getHostAddress()); + } + } finally { + proxy.lock.writeLock().unlock(); + } + } + if (jdbcUrl.getOptions().autoReconnect) { + reconnectFailedConnection(new SearchFilter(false, true, false, true)); + //connection established, no need to send Exception ! + log.finest("switching to master connection"); + proxy.lock.writeLock().lock(); + try { + syncConnection(this.secondaryProtocol, this.masterProtocol); + currentProtocol = this.masterProtocol; + } finally { + proxy.lock.writeLock().unlock(); + } + log.fine("current connection is now master"); + return; + } + launchFailLoopIfNotlaunched(false); + throwFailoverMessage(new QueryException("master " + masterProtocol.getHostAddress() + " connection failed"), false); + } + } + } + } + + /** + * to handle the newly detected failover on the master connection + * + * @param method the initial called method + * @param args the initial args + * @return an object to indicate if the previous Exception must be thrown, or the object resulting if a failover worked + * @throws Throwable if failover has not been catch + */ + public HandleErrorResult primaryFail(Method method, Object[] args) throws Throwable { + boolean alreadyClosed = !masterProtocol.isConnected(); + + //try to reconnect automatically only time before looping + try { + if (masterProtocol != null && masterProtocol.isConnected() && masterProtocol.ping()) { + if (log.isLoggable(Level.FINE)) + log.fine("Primary node [" + masterProtocol.getHostAddress().toString() + "] connection re-established"); + + // if in transaction cannot be sure that the last query has been received by server of not, so rollback. + if (masterProtocol.inTransaction()) { + masterProtocol.rollback(); + } + return new HandleErrorResult(true); + } + } catch (QueryException e) { + proxy.lock.writeLock().lock(); + try { + masterProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + + if (setMasterHostFail()) addToBlacklist(masterProtocol.getHostAddress()); + } + + //fail on slave if parameter permit so + if (jdbcUrl.getOptions().failOnReadOnly) { + //in multiHost, switch to secondary if active, even if in a current transaction -> will throw an exception + if (!isSecondaryHostFail()) { + try { + if (this.secondaryProtocol != null && this.secondaryProtocol.ping()) { + log.finest("switching to secondary connection"); + syncConnection(masterProtocol, this.secondaryProtocol); + proxy.lock.writeLock().lock(); + try { + currentProtocol = this.secondaryProtocol; + } finally { + proxy.lock.writeLock().unlock(); + } + launchFailLoopIfNotlaunched(false); + try { + return relaunchOperation(method, args); + } catch (Exception e) { + log.log(Level.FINEST, "relaunchOperation failed", e); + } + return new HandleErrorResult(); + } else log.finest("ping failed on secondary"); + } catch (Exception e) { + if (setSecondaryHostFail()) addToBlacklist(this.secondaryProtocol.getHostAddress()); + if (secondaryProtocol.isConnected()) { + proxy.lock.writeLock().lock(); + try { + secondaryProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + } + log.log(Level.FINEST, "ping on secondary failed"); + } + } + } + + try { + reconnectFailedConnection(new SearchFilter(true, jdbcUrl.getOptions().failOnReadOnly, true, jdbcUrl.getOptions().failOnReadOnly)); + if (isMasterHostFail()) launchFailLoopIfNotlaunched(true); + if (alreadyClosed) return relaunchOperation(method, args); + return new HandleErrorResult(true); + } catch (Exception e) { + launchFailLoopIfNotlaunched(true); + return new HandleErrorResult(); + } + } + + + public void reconnect() throws QueryException { + SearchFilter filter; + if (currentReadOnlyAsked.get()) { + filter = new SearchFilter(true, true, true, true); + } else { + filter = new SearchFilter(true, jdbcUrl.getOptions().failOnReadOnly, true, jdbcUrl.getOptions().failOnReadOnly); + } + reconnectFailedConnection(filter); + } + + + /** + * to handle the newly detected failover on the secondary connection + * + * @param method the initial called method + * @param args the initial args + * @return an object to indicate if the previous Exception must be thrown, or the object resulting if a failover worked + * @throws Throwable if failover has not catch error + */ + public HandleErrorResult secondaryFail(Method method, Object[] args) throws Throwable { + try { + if (this.secondaryProtocol != null && secondaryProtocol.isConnected() && this.secondaryProtocol.ping()) { + if (log.isLoggable(Level.FINE)) + log.fine("Secondary node [" + this.secondaryProtocol.getHostAddress().toString() + "] connection re-established"); + return relaunchOperation(method, args); + } + } catch (Exception e) { + log.finest("ping fail on secondary"); + proxy.lock.writeLock().lock(); + try { + secondaryProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + + if (setSecondaryHostFail()) addToBlacklist(this.secondaryProtocol.getHostAddress()); + } + + if (!isMasterHostFail()) { + try { + if (masterProtocol != null) { + this.masterProtocol.ping(); //check that master is on before switching to him + log.finest("switching to master connection"); + syncConnection(secondaryProtocol, masterProtocol); + proxy.lock.writeLock().lock(); + try { + currentProtocol = masterProtocol; + } finally { + proxy.lock.writeLock().unlock(); + } + launchFailLoopIfNotlaunched(true); //launch reconnection loop + return relaunchOperation(method, args); //now that we are on master, relaunched result if the result was not crashing the master + } + } catch (Exception e) { + log.finest("ping fail on master"); + if (setMasterHostFail()) { + addToBlacklist(masterProtocol.getHostAddress()); + if (masterProtocol.isConnected()) { + proxy.lock.writeLock().lock(); + try { + masterProtocol.close(); + } finally { + proxy.lock.writeLock().unlock(); + } + } + } + } + } + + try { + reconnectFailedConnection(new SearchFilter(true, true, true, true)); + if (!isSecondaryHostFail()) { + if (log.isLoggable(Level.FINE)) + log.fine("SQL Secondary node [" + this.masterProtocol.getHostAddress().toString() + "] connection re-established"); + } else { + log.finest("switching to master connection"); + syncConnection(this.secondaryProtocol, this.masterProtocol); + proxy.lock.writeLock().lock(); + try { + currentProtocol = this.masterProtocol; + } finally { + proxy.lock.writeLock().unlock(); + } + } + return relaunchOperation(method, args); //now that we are reconnect, relaunched result if the result was not crashing the node + } catch (Exception ee) { + launchFailLoopIfNotlaunched(false); + return new HandleErrorResult(); + } + } + + + /** + * private class to chech of currents connections are still ok. + */ + protected class PingLoop implements Runnable { + MastersSlavesListener listener; + + public PingLoop(MastersSlavesListener listener) { + this.listener = listener; + } + + public void run() { + if (lastQueryTime + jdbcUrl.getOptions().validConnectionTimeout * 1000 < System.currentTimeMillis()) { + log.finest("PingLoop run "); + if (!isMasterHostFail()) { + log.finest("PingLoop run, master not seen failed"); + boolean masterFail = false; + try { + + if (masterProtocol != null && masterProtocol.isConnected()) { + checkIfTypeHaveChanged(null); + } else { + masterFail = true; + } + } catch (QueryException e) { + log.log(Level.FINEST, "PingLoop ping to master error", e); + masterFail = true; + } + + if (masterFail) { + log.finest("PingLoop master failed -> will loop to found it"); + if (setMasterHostFail()) { + try { + listener.primaryFail(null, null); + } catch (Throwable t) { + //do nothing + } + } + } + } + } + } + } + + public void checkIfTypeHaveChanged(SearchFilter searchFilter) throws QueryException { + if (masterProtocol.ping()) { + log.finest("PingLoop master ping ok"); + } + } + + + /** + * Throw a human readable message after a failoverException + * + * @param queryException internal error + * @param reconnected connection status + * @throws QueryException error with failover information + */ + @Override + public void throwFailoverMessage(QueryException queryException, boolean reconnected) throws QueryException { + boolean connectionTypeMaster = true; + HostAddress hostAddress = (masterProtocol != null) ? masterProtocol.getHostAddress() : null; + if (currentReadOnlyAsked.get()) { + connectionTypeMaster = false; + hostAddress = (secondaryProtocol != null) ? secondaryProtocol.getHostAddress() : null; + } + + String firstPart = "Communications link failure with " + (connectionTypeMaster ? "primary" : "secondary") + ((hostAddress != null) ? " host " + hostAddress.host + ":" + hostAddress.port : "") + ". "; + String error = ""; + if (jdbcUrl.getOptions().autoReconnect || (!isMasterHostFail() && !isSecondaryHostFail())) { + if ((connectionTypeMaster && isMasterHostFail()) || (!connectionTypeMaster && isSecondaryHostFail())) + error += " Driver will reconnect automatically in a few millisecond or during next query if append before"; + else error += " Driver as successfully reconnect connection"; + } else { + if (reconnected) { + error += " Driver as reconnect connection"; + } else { + if (currentConnectionAttempts.get() > jdbcUrl.getOptions().retriesAllDown) { + error += " Driver will not try to reconnect (too much failure > " + jdbcUrl.getOptions().retriesAllDown + ")"; + } else { + if (shouldReconnect()) { + error += " Driver will try to reconnect automatically in a few millisecond or during next query if append before"; + } else { + long longestFail = isMasterHostFail() ? (isSecondaryHostFail() ? Math.min(getMasterHostFailTimestamp(), getSecondaryHostFailTimestamp()) : getMasterHostFailTimestamp()) : getSecondaryHostFailTimestamp(); + long nextReconnectionTime = jdbcUrl.getOptions().secondsBeforeRetryMaster * 1000 - (System.currentTimeMillis() - longestFail); + if (jdbcUrl.getOptions().secondsBeforeRetryMaster > 0) { + if (jdbcUrl.getOptions().queriesBeforeRetryMaster > 0) { + error += " Driver will try to reconnect " + (connectionTypeMaster ? "primary" : "secondary") + " after " + nextReconnectionTime + " milliseconds or after " + (jdbcUrl.getOptions().queriesBeforeRetryMaster - queriesSinceFailover.get()) + " query(s)"; + } else { + error += " Driver will try to reconnect " + (connectionTypeMaster ? "primary" : "secondary") + " after " + nextReconnectionTime + " milliseconds"; + } + } else { + if (jdbcUrl.getOptions().queriesBeforeRetryMaster > 0) { + error += " Driver will try to reconnect " + (connectionTypeMaster ? "primary" : "secondary") + " after " + (jdbcUrl.getOptions().queriesBeforeRetryMaster - queriesSinceFailover.get()) + " query(s)"; + } else { + error += " Driver will not try to reconnect automatically"; + } + } + } + + } + } + } + if (queryException == null) { + throw new QueryException(firstPart + error, (short) -1, SQLExceptionMapper.SQLStates.CONNECTION_EXCEPTION.getSqlState()); + } else { + error = queryException.getMessage() + ". " + error; + queryException.setMessage(firstPart + error); + throw queryException; + } + } +} diff --git a/src/main/java/org/mariadb/jdbc/internal/mysql/listener/tools/SearchFilter.java b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/tools/SearchFilter.java new file mode 100644 index 000000000..ec6d3d1f0 --- /dev/null +++ b/src/main/java/org/mariadb/jdbc/internal/mysql/listener/tools/SearchFilter.java @@ -0,0 +1,136 @@ +/* +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.listener.tools; + +public class SearchFilter { + boolean searchForMaster; + boolean searchForSlave; + boolean fineIfFoundOnlyMaster; + boolean fineIfFoundOnlySlave; + boolean initialConnection; + boolean uniqueLoop; + + public SearchFilter (boolean searchForMaster, boolean searchForSlave) { + this.searchForMaster = searchForMaster; + this.searchForSlave = searchForSlave; + } + + public SearchFilter(boolean searchForMaster, boolean searchForSlave, boolean fineIfFoundOnlyMaster, boolean fineIfFoundOnlySlave) { + this.searchForMaster = searchForMaster; + this.searchForSlave = searchForSlave; + this.fineIfFoundOnlyMaster = fineIfFoundOnlyMaster; + this.fineIfFoundOnlySlave = fineIfFoundOnlySlave; + } + + public SearchFilter(boolean searchForMaster, boolean searchForSlave, boolean initialConnection) { + this.searchForMaster = searchForMaster; + this.searchForSlave = searchForSlave; + this.initialConnection = initialConnection; + } + + public boolean isInitialConnection() { + return initialConnection; + } + + public void setInitialConnection(boolean initialConnection) { + this.initialConnection = initialConnection; + } + + public boolean isFineIfFoundOnlyMaster() { + return fineIfFoundOnlyMaster; + } + + public void setFineIfFoundOnlyMaster(boolean fineIfFoundOnlyMaster) { + this.fineIfFoundOnlyMaster = fineIfFoundOnlyMaster; + } + + public boolean isFineIfFoundOnlySlave() { + return fineIfFoundOnlySlave; + } + + public void setFineIfFoundOnlySlave(boolean fineIfFoundOnlySlave) { + this.fineIfFoundOnlySlave = fineIfFoundOnlySlave; + } + + public boolean isSearchForMaster() { + return searchForMaster; + } + + public void setSearchForMaster(boolean searchForMaster) { + this.searchForMaster = searchForMaster; + } + + public boolean isSearchForSlave() { + return searchForSlave; + } + + public void setSearchForSlave(boolean searchForSlave) { + this.searchForSlave = searchForSlave; + } + + public boolean isUniqueLoop() { + return uniqueLoop; + } + + public void setUniqueLoop(boolean uniqueLoop) { + this.uniqueLoop = uniqueLoop; + } + + @Override + public String toString() { + return "SearchFilter{" + + "searchForMaster=" + searchForMaster + + ", searchForSlave=" + searchForSlave + + ", fineIfFoundOnlyMaster=" + fineIfFoundOnlyMaster + + ", fineIfFoundOnlySlave=" + fineIfFoundOnlySlave + + ", initialConnection=" + initialConnection + + ", uniqueLoop=" + uniqueLoop + + '}'; + } +} diff --git a/src/main/resources/Version.java.template b/src/main/resources/Version.java.template index dd657a5d6..a8ea7dd89 100644 --- a/src/main/resources/Version.java.template +++ b/src/main/resources/Version.java.template @@ -1,7 +1,10 @@ package org.mariadb.jdbc; public final class Version { - public static final String build_time="@buildtime@"; - public static final String pomversion="@pomversion@"; + public static final String version = "@version"; + public static final int majorVersion = @majorVersion; + public static final int minorVersion = @minorVersion; + public static final int patchVersion = @patchVersion; + public static final String qualifier = "@qualifier"; } \ No newline at end of file diff --git a/src/test/java/org/mariadb/jdbc/BaseTest.java b/src/test/java/org/mariadb/jdbc/BaseTest.java index 7cd2efd06..8dd4d2e24 100644 --- a/src/test/java/org/mariadb/jdbc/BaseTest.java +++ b/src/test/java/org/mariadb/jdbc/BaseTest.java @@ -1,13 +1,16 @@ package org.mariadb.jdbc; -import org.junit.After; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; +import org.junit.*; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.mariadb.jdbc.failover.BaseMultiHostTest; +import org.mariadb.jdbc.internal.mysql.Protocol; import java.io.PrintWriter; import java.io.StringWriter; +import java.lang.reflect.Method; import java.net.InetAddress; import java.net.UnknownHostException; import java.sql.*; @@ -23,7 +26,7 @@ @Ignore public class BaseTest { - protected static Logger log = Logger.getLogger("org.maria.jdbc"); + protected static Logger log = Logger.getLogger("org.mariadb.jdbc"); protected Connection connection; protected static String connU; protected static String connURI; @@ -34,45 +37,54 @@ public class BaseTest { protected static String password; protected static String parameters; protected static final String mDefUrl = "jdbc:mysql://localhost:3306/test?user=root"; + protected static boolean testSingleHost; @BeforeClass public static void beforeClassBaseTest() { String url = System.getProperty("dbUrl", mDefUrl); + testSingleHost = Boolean.parseBoolean(System.getProperty("testSingleHost", "true")); JDBCUrl jdbcUrl = JDBCUrl.parse(url); - String logLevel = System.getProperty("logLevel"); - if (logLevel != null) { - if (log.getHandlers().length == 0) { - ConsoleHandler consoleHandler = new ConsoleHandler(); - consoleHandler.setFormatter(new CustomFormatter()); - consoleHandler.setLevel(Level.parse(logLevel)); - log.addHandler(consoleHandler); - log.setLevel(Level.FINE); - } - } - - hostname = jdbcUrl.getHostname(); - port = jdbcUrl.getPort(); + hostname = jdbcUrl.getHostAddresses().get(0).host; + port = jdbcUrl.getHostAddresses().get(0).port; database = jdbcUrl.getDatabase(); username = jdbcUrl.getUsername(); password = jdbcUrl.getPassword(); log.fine("Properties parsed from JDBC URL - hostname: " + hostname + ", port: " + port + ", database: " + database + ", username: " + username + ", password: " + password); - - if (database != null && "".equals(username)) { - String[] tokens = database.contains("?") ? database.split("\\?") : null; - if (tokens != null) { - database = tokens[0]; - String[] paramTokens = tokens[1].split("&"); - username = paramTokens[0].startsWith("user=") ? paramTokens[0].substring(5) : null; - if (paramTokens.length > 1) { - password = paramTokens[0].startsWith("password=") ? paramTokens[1].substring(9) : null; - } - } - } + setURI(); } - + + @Before + public void init() throws SQLException { + Assume.assumeTrue(testSingleHost); + } + + @Rule + public TestRule watcher = new TestWatcher() { + protected void starting(Description description) { + log.fine("Starting test: " + description.getMethodName()); + } + + protected void finished(Description description) { + log.fine("finished test: " + description.getMethodName()); + } + }; + + public void assureBlackList(Connection connection) { + try { + Protocol protocol = getProtocolFromConnection(connection); + protocol.getProxy().getListener().getBlacklist().clear(); + } catch (Throwable e) { } + } + + protected Protocol getProtocolFromConnection(Connection conn) throws Throwable { + + Method getProtocol = MySQLConnection.class.getDeclaredMethod("getProtocol", new Class[0]); + getProtocol.setAccessible(true); + return (Protocol) getProtocol.invoke(conn); + } private static void setURI() { connU = "jdbc:mysql://" + hostname + ":" + port + "/" + database; connURI = connU + "?user=" + username @@ -207,7 +219,6 @@ boolean checkMaxAllowedPacketMore40m(String testName) throws SQLException { return true; } - //does the user have super privileges or not? boolean hasSuperPrivilege(String testName) throws SQLException { @@ -285,37 +296,3 @@ boolean isMariadbServer() throws SQLException { } } - -class CustomFormatter extends Formatter { - private static final String format = "[%1$tT] %4$s: %2$s - %5$s %6$s%n"; - private final java.util.Date dat = new java.util.Date(); - public synchronized String format(LogRecord record) { - dat.setTime(record.getMillis()); - String source; - if (record.getSourceClassName() != null) { - source = record.getSourceClassName(); - if (record.getSourceMethodName() != null) { - source += " " + record.getSourceMethodName(); - } - } else { - source = record.getLoggerName(); - } - String message = formatMessage(record); - String throwable = ""; - if (record.getThrown() != null) { - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - pw.println(); - record.getThrown().printStackTrace(pw); - pw.close(); - throwable = sw.toString(); - } - return String.format(format, - dat, - source, - record.getLoggerName(), - record.getLevel().getName(), - message, - throwable); - } -} \ No newline at end of file diff --git a/src/test/java/org/mariadb/jdbc/CallableStatementTest.java b/src/test/java/org/mariadb/jdbc/CallableStatementTest.java index 7d028de38..19755448b 100644 --- a/src/test/java/org/mariadb/jdbc/CallableStatementTest.java +++ b/src/test/java/org/mariadb/jdbc/CallableStatementTest.java @@ -6,7 +6,7 @@ import java.sql.*; -import static junit.framework.Assert.*; +import static org.junit.Assert.*; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.*; @@ -134,7 +134,7 @@ public void withStrangeParameter() throws SQLException { ResultSet rs = stmt.executeQuery(); assertTrue(rs.next()); double res = rs.getDouble(1); - assertEquals(expected, res); + assertEquals(expected, res, 0); // now fail due to three decimals double tooMuch = 34.987; stmt.setDouble("a", tooMuch); diff --git a/src/test/java/org/mariadb/jdbc/CancelTest.java b/src/test/java/org/mariadb/jdbc/CancelTest.java index fb2fc3a8b..61f9fdede 100644 --- a/src/test/java/org/mariadb/jdbc/CancelTest.java +++ b/src/test/java/org/mariadb/jdbc/CancelTest.java @@ -1,9 +1,13 @@ package org.mariadb.jdbc; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.sql.*; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import static org.junit.Assert.assertEquals; @@ -12,15 +16,28 @@ public class CancelTest extends BaseTest { public void cancelSupported() throws SQLException { requireMinimumVersion(5,0); } - @Test(expected = SQLTransientException.class) + @Test public void cancelTest() throws SQLException{ + Connection tmpConnection = null; + try { + tmpConnection = openNewConnection(connURI, new Properties()); + Statement stmt = tmpConnection.createStatement(); + ExecutorService exec = Executors.newFixedThreadPool(1); + //check blacklist shared + exec.execute(new CancelThread(stmt)); + stmt.execute("select * from information_schema.columns, information_schema.tables"); - Statement stmt = connection.createStatement(); - new CancelThread(stmt).start(); - stmt.execute("select * from information_schema.columns, information_schema.tables, information_schema.table_constraints"); + //wait for thread endings + exec.shutdown(); + Assert.fail(); + } catch (SQLException e) { + + }finally { + tmpConnection.close(); + } } - private static class CancelThread extends Thread { + private static class CancelThread implements Runnable { private final Statement stmt; public CancelThread(Statement stmt) { diff --git a/src/test/java/org/mariadb/jdbc/CatalogTest.java b/src/test/java/org/mariadb/jdbc/CatalogTest.java index c73f6aea9..b3cfdfcfa 100644 --- a/src/test/java/org/mariadb/jdbc/CatalogTest.java +++ b/src/test/java/org/mariadb/jdbc/CatalogTest.java @@ -7,7 +7,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class CatalogTest extends BaseTest { static { Logger.getLogger("").setLevel(Level.FINEST); } diff --git a/src/test/java/org/mariadb/jdbc/ConnectionPoolTest.java b/src/test/java/org/mariadb/jdbc/ConnectionPoolTest.java index 1a2a572f0..d3ed27c7e 100644 --- a/src/test/java/org/mariadb/jdbc/ConnectionPoolTest.java +++ b/src/test/java/org/mariadb/jdbc/ConnectionPoolTest.java @@ -40,6 +40,95 @@ public void testConnectionWithApacheDBCP() throws SQLException { connection.close(); dataSource.close(); } + + /* + * + + @Test + public void testTimeoutsInPool() throws SQLException, InterruptedException { + org.apache.commons.dbcp.BasicDataSource dataSource; + dataSource = new org.apache.commons.dbcp.BasicDataSource(); + dataSource.setUrl("jdbc:mysql://" + hostname + ":"+port+"/test?useCursorFetch=true&useTimezone=true&useLegacyDatetimeCode=false&serverTimezone=UTC"); + dataSource.setUsername(username); + dataSource.setPassword(password); + // dataSource.setMaxActive(10); + // dataSource.setMinIdle(10); //keep 10 connections open + // dataSource.setValidationQuery("SELECT 1"); + dataSource.setMaxActive(50); + dataSource.setLogAbandoned(true); + dataSource.setRemoveAbandoned(true); + dataSource.setRemoveAbandonedTimeout(300); + dataSource.setAccessToUnderlyingConnectionAllowed(true); + dataSource.setMinEvictableIdleTimeMillis(1800000); + dataSource.setTimeBetweenEvictionRunsMillis(-1); + dataSource.setNumTestsPerEvictionRun(3); + + // adjust server wait timeout to 1 second + // Statement stmt1 = conn1.createStatement(); + // stmt1.execute("set session wait_timeout=1"); + + + try { + Connection conn = dataSource.getConnection(); + log.fine("autocommit: " + conn.getAutoCommit()); + Statement stmt = conn.createStatement(); + stmt.executeUpdate("drop table if exists t3"); + stmt.executeUpdate("create table t3(message text)"); + conn.close(); + } catch (SQLException e1) { + e1.printStackTrace(); + } + + InsertThread ins1 = new InsertThread(10000, dataSource); + Thread thread1 = new Thread(ins1); + thread1.start(); + InsertThread ins2 = new InsertThread(10000, dataSource); + Thread thread2 = new Thread(ins2); + thread2.start(); + InsertThread ins3 = new InsertThread(10000, dataSource); + Thread thread3 = new Thread(ins3); + thread3.start(); + InsertThread ins4 = new InsertThread(10000, dataSource); + Thread thread4 = new Thread(ins4); + thread4.start(); + InsertThread ins5 = new InsertThread(10000, dataSource); + Thread thread5 = new Thread(ins5); + thread5.start(); + InsertThread ins6 = new InsertThread(10000, dataSource); + Thread thread6 = new Thread(ins6); + thread6.start(); + InsertThread ins7 = new InsertThread(10000, dataSource); + Thread thread7 = new Thread(ins7); + thread7.start(); + InsertThread ins8 = new InsertThread(10000, dataSource); + Thread thread8 = new Thread(ins8); + thread8.start(); + InsertThread ins9 = new InsertThread(10000, dataSource); + Thread thread9 = new Thread(ins9); + thread9.start(); + InsertThread ins10 = new InsertThread(10000, dataSource); + Thread thread10 = new Thread(ins10); + thread10.start(); + + // wait for threads to finish + while (thread1.isAlive() || thread2.isAlive() || thread3.isAlive() || thread4.isAlive() || thread5.isAlive() || thread6.isAlive() || thread7.isAlive() || thread8.isAlive() || thread9.isAlive() || thread10.isAlive()) + { + //keep on waiting for threads to finish + } + + // wait for 70 seconds so that the server times out the connections + Thread.sleep(70000); // Wait for the server to kill the connections + + // do something + Statement stmt1 = dataSource.getConnection().createStatement(); + stmt1.execute("SELECT COUNT(*) FROM t3"); + + // close data source + dataSource.close(); + + } */ + + /** * This test case simulates how the Apache DBCP connection pools works. It is written so it @@ -52,7 +141,7 @@ public void testConnectionWithSimululatedApacheDBCP() throws SQLException { Properties props = new Properties(); props.put("user", username); - props.put("password", password); + props.put("password", (password==null)?"":password); //A connection pool typically has a connection factor that stored everything needed to //create a Connection. Here I create a factory that stores URL, username and password. diff --git a/src/test/java/org/mariadb/jdbc/ConnectionTest.java b/src/test/java/org/mariadb/jdbc/ConnectionTest.java index 689aa6250..72cce69e6 100644 --- a/src/test/java/org/mariadb/jdbc/ConnectionTest.java +++ b/src/test/java/org/mariadb/jdbc/ConnectionTest.java @@ -15,7 +15,9 @@ import org.junit.Assume; import org.junit.Test; import org.mariadb.jdbc.internal.common.query.MySQLQuery; +import org.mariadb.jdbc.internal.mysql.FailoverProxy; import org.mariadb.jdbc.internal.mysql.MySQLProtocol; +import org.mariadb.jdbc.internal.mysql.Protocol; public class ConnectionTest extends BaseTest { @@ -235,34 +237,4 @@ public void isValid_connectionThatTimesOutByServer() throws SQLException, statement.close(); } - /** - * CONJ-120 Fix Connection.isValid method - * - * @throws Exception - */ - @Test - public void isValid_connectionThatIsKilledExternally() throws Exception { - long threadId = getServerThreadId(); - Connection killerConnection = openNewConnection(); - Statement killerStatement = killerConnection.createStatement(); - killerStatement.execute("KILL CONNECTION " + threadId); - killerConnection.close(); - boolean isValid = connection.isValid(0); - assertFalse(isValid); - } - - /** - * Reflection magic to extract the connection thread id assigned by the - * server - */ - private long getServerThreadId() throws Exception { - Field protocolField = org.mariadb.jdbc.MySQLConnection.class.getDeclaredField("protocol"); - protocolField.setAccessible(true); - MySQLProtocol protocol = (MySQLProtocol) protocolField.get(connection); - Field serverThreadIdField = MySQLProtocol.class.getDeclaredField("serverThreadId"); - serverThreadIdField.setAccessible(true); - long threadId = serverThreadIdField.getLong(protocol); - return threadId; - } - } diff --git a/src/test/java/org/mariadb/jdbc/DataSourceTest.java b/src/test/java/org/mariadb/jdbc/DataSourceTest.java index e3763ac13..38513246f 100644 --- a/src/test/java/org/mariadb/jdbc/DataSourceTest.java +++ b/src/test/java/org/mariadb/jdbc/DataSourceTest.java @@ -1,5 +1,7 @@ package org.mariadb.jdbc; +import org.junit.Assert; +import org.junit.Assume; import org.junit.BeforeClass; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -11,7 +13,7 @@ import org.junit.Test; public class DataSourceTest extends BaseTest { - protected static final String defConnectToIP = "127.0.0.1"; + protected static final String defConnectToIP = null; protected static String connectToIP; @BeforeClass @@ -39,7 +41,20 @@ public void testDataSource2() throws SQLException { connection.close(); } } - + + @Test + public void testDataSourceEmpty() throws SQLException { + MySQLDataSource ds = new MySQLDataSource(); + ds.setDatabaseName(database); + ds.setPort(port); + ds.setServerName(hostname); + Connection connection = ds.getConnection(username, password); + try { + assertEquals(connection.isValid(0),true); + }finally { + connection.close(); + } + } /** * CONJ-80 * @throws SQLException @@ -56,37 +71,50 @@ public void setDatabaseNameTest() throws SQLException { connection.createStatement().execute("DROP DATABASE IF EXISTS test2"); connection.close(); } - + /** * CONJ-80 * @throws SQLException */ @Test public void setServerNameTest() throws SQLException { + Assume.assumeTrue(connectToIP != null); MySQLDataSource ds = new MySQLDataSource(hostname, port, database); Connection connection = ds.getConnection(username, password); ds.setServerName(connectToIP); connection = ds.getConnection(username, password); connection.close(); } - + /** * CONJ-80 * @throws SQLException */ - @Test(expected=SQLException.class) // unless port 3307 can be used + @Test // unless port 3307 can be used public void setPortTest() throws SQLException { + + MySQLDataSource ds = new MySQLDataSource(hostname, port, database); - Connection connection = ds.getConnection(username, password); - ds.setPort(3307); - connection = ds.getConnection(username, password); - connection.close(); + Connection connection2 = ds.getConnection(username, password); + //delete blacklist, because can failover on 3306 is filled + assureBlackList(connection2); + connection2.close(); + + ds.setPort(3307); + + //must throw SQLException + try { + ds.getConnection(username, password); + Assert.fail(); + } catch (SQLException e) { + log.fine("port error : " +e.getMessage()); + } } - + /** * CONJ-123: * Session variables lost and exception if set via MySQLDataSource.setProperties/setURL - * @throws SQLException + * @throws SQLException */ @Test public void setPropertiesTest() throws SQLException { diff --git a/src/test/java/org/mariadb/jdbc/DatabaseMetadataTest.java b/src/test/java/org/mariadb/jdbc/DatabaseMetadataTest.java index 88465b8ac..208927e72 100644 --- a/src/test/java/org/mariadb/jdbc/DatabaseMetadataTest.java +++ b/src/test/java/org/mariadb/jdbc/DatabaseMetadataTest.java @@ -7,8 +7,8 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.fail; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/org/mariadb/jdbc/DatatypeTest.java b/src/test/java/org/mariadb/jdbc/DatatypeTest.java index 3b4011f1f..0b141e377 100644 --- a/src/test/java/org/mariadb/jdbc/DatatypeTest.java +++ b/src/test/java/org/mariadb/jdbc/DatatypeTest.java @@ -9,7 +9,7 @@ import java.sql.ResultSet; import java.sql.Types; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class DatatypeTest extends BaseTest { diff --git a/src/test/java/org/mariadb/jdbc/DateTest.java b/src/test/java/org/mariadb/jdbc/DateTest.java index 92daffac6..afc23aec7 100644 --- a/src/test/java/org/mariadb/jdbc/DateTest.java +++ b/src/test/java/org/mariadb/jdbc/DateTest.java @@ -1,7 +1,7 @@ package org.mariadb.jdbc; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.sql.Date; import java.sql.PreparedStatement; @@ -17,7 +17,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import junit.framework.Assert; +import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; @@ -196,6 +196,8 @@ public void javaUtilDateInPreparedStatementAsTimeStamp() throws Exception { @Test public void nullTimestampTest() throws SQLException { + connection.createStatement().execute("drop table if exists dtest"); + connection.createStatement().execute("create table dtest (d date)"); PreparedStatement ps = connection.prepareStatement("insert into dtest values(null)"); ps.executeUpdate(); ResultSet rs = connection.createStatement().executeQuery("select * from dtest where d is null"); @@ -207,6 +209,8 @@ public void nullTimestampTest() throws SQLException { @SuppressWarnings( "deprecation" ) @Test public void javaUtilDateInPreparedStatementAsDate() throws Exception { + connection.createStatement().execute("drop table if exists dtest"); + connection.createStatement().execute("create table dtest (d date)"); java.util.Date d = Calendar.getInstance(TimeZone.getDefault()).getTime(); PreparedStatement ps = connection.prepareStatement("insert into dtest values(?)"); ps.setObject(1, d, Types.DATE); @@ -255,7 +259,8 @@ public void serverTimezone() throws Exception { java.sql.Timestamp ts = rs.getTimestamp(1); long differenceToGMT = ts.getTime() - now.getTime(); long diff = Math.abs(differenceToGMT - offset); - assertTrue(diff < 2000); /* query take less than a second */ + log.fine("diff : "+diff); + assertTrue(diff < 5000); /* query take less than a second but taking in accout server and client time second diff ... */ ps = connection.prepareStatement("select utc_timestamp(), ?"); ps.setObject(1,now); @@ -263,7 +268,8 @@ public void serverTimezone() throws Exception { rs.next(); ts = rs.getTimestamp(1); java.sql.Timestamp ts2 = rs.getTimestamp(2); - assertTrue(Math.abs(ts.getTime() - ts2.getTime()) < 1000); /* query take less than a second */ + long diff2 = Math.abs(ts.getTime() - ts2.getTime()); + assertTrue(diff2 < 5000); /* query take less than a second */ } /** diff --git a/src/test/java/org/mariadb/jdbc/DriverTest.java b/src/test/java/org/mariadb/jdbc/DriverTest.java index 2079bf969..de439f2ff 100644 --- a/src/test/java/org/mariadb/jdbc/DriverTest.java +++ b/src/test/java/org/mariadb/jdbc/DriverTest.java @@ -15,7 +15,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static junit.framework.Assert.*; +import static org.junit.Assert.*; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -303,14 +303,14 @@ public void testConnectNoDB() throws Exception{ @Test public void testConnectorJURL() { JDBCUrl url = JDBCUrl.parse("jdbc:mysql://localhost/test"); - assertEquals("localhost", url.getHostname()); + assertEquals("localhost", url.getHostAddresses().get(0).host); assertEquals("test", url.getDatabase()); - assertEquals(3306,url.getPort()); + assertEquals(3306,url.getHostAddresses().get(0).port); url = JDBCUrl.parse("jdbc:mysql://localhost:3307/test"); - assertEquals("localhost", url.getHostname()); + assertEquals("localhost", url.getHostAddresses().get(0).host); assertEquals("test", url.getDatabase()); - assertEquals(3307,url.getPort()); + assertEquals(3307,url.getHostAddresses().get(0).port); } @@ -1116,7 +1116,7 @@ public void testConnectWithDB() throws SQLException { } catch (Exception e) {} String oldDb = database; String oldParams = parameters; - setParameters("&createDB=true"); + setParameters("&createDatabaseIfNotExist=true"); setDatabase("test_testdrop"); DatabaseMetaData dbmd = connection.getMetaData(); ResultSet rs = dbmd.getCatalogs(); @@ -1308,34 +1308,6 @@ public void unsignedTest() throws Exception { assertNotNull(rs.getLong("unsignedtest.a")); } - @Test - public void autoreconnect() throws Exception { - setConnection("&autoReconnect=true"); - ResultSet rs= connection.createStatement().executeQuery("select connection_id()"); - rs.next(); - long connectionId = rs.getLong(1); - rs.close(); - - connection.createStatement().execute("set wait_timeout=1"); - Thread.sleep(3000); - - boolean success = false; - for (int i=0; i < 2; i++) { - try { - rs = connection.createStatement().executeQuery("select 1"); - rs.close(); - success = true; - break; - } catch (Exception e) { - // eat exception - } - } - assertTrue(success); - rs = connection.createStatement().executeQuery("select connection_id()"); - rs.next(); - long connectionId2 = rs.getLong(1); - assertNotSame(connectionId, connectionId2); - } @Test public void useSSL() throws Exception { @@ -1557,7 +1529,7 @@ public void localSocket() throws Exception { ResultSet rs = st.executeQuery("select @@version_compile_os,@@socket"); if (!rs.next()) return; - + log.info ("os:"+rs.getString(1) + " path:"+rs.getString(2)); String os = rs.getString(1); if (os.toLowerCase().startsWith("win")) return; @@ -1640,7 +1612,7 @@ public void emptyBatch() throws Exception { @Test public void createDbWithSpacesTest() throws SQLException { String oldDb = database; - setParameters("&createDB=true"); + setParameters("&createDatabaseIfNotExist=true"); setDatabase("test with spaces"); DatabaseMetaData dbmd = connection.getMetaData(); ResultSet rs = dbmd.getCatalogs(); diff --git a/src/test/java/org/mariadb/jdbc/JdbcParserTest.java b/src/test/java/org/mariadb/jdbc/JdbcParserTest.java new file mode 100644 index 000000000..ce4ef85f2 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/JdbcParserTest.java @@ -0,0 +1,226 @@ +package org.mariadb.jdbc; + +import org.junit.Assert; +import org.junit.Test; +import org.mariadb.jdbc.internal.common.UrlHAMode; + +import java.util.Properties; + +public class JdbcParserTest { + + @Test + public void testOptionTakeDefault() throws Throwable { + JDBCUrl jdbc = JDBCUrl.parse("jdbc:mysql://localhost/test"); + Assert.assertNull(jdbc.getOptions().connectTimeout); + Assert.assertTrue(jdbc.getOptions().validConnectionTimeout == 120); + Assert.assertFalse(jdbc.getOptions().autoReconnect); + Assert.assertNull(jdbc.getOptions().user); + Assert.assertFalse(jdbc.getOptions().createDatabaseIfNotExist); + Assert.assertNull(jdbc.getOptions().socketTimeout); + + } + + @Test + public void testOptionTakeDefaultAurora() throws Throwable { + JDBCUrl jdbc = JDBCUrl.parse("jdbc:mysql:aurora://localhost/test"); + Assert.assertNull(jdbc.getOptions().connectTimeout); + Assert.assertTrue(jdbc.getOptions().validConnectionTimeout == 120); + Assert.assertFalse(jdbc.getOptions().autoReconnect); + Assert.assertNull(jdbc.getOptions().user); + Assert.assertFalse(jdbc.getOptions().createDatabaseIfNotExist); + Assert.assertTrue(jdbc.getOptions().socketTimeout.intValue() == 10000); + } + + @Test + public void testOptionParse() throws Throwable { + JDBCUrl jdbc = JDBCUrl.parse("jdbc:mysql://localhost/test?user=root&password=toto&createDB=true&autoReconnect=true&validConnectionTimeout=2&connectTimeout=5"); + Assert.assertTrue(jdbc.getOptions().connectTimeout == 5); + Assert.assertTrue(jdbc.getOptions().validConnectionTimeout == 2); + Assert.assertTrue(jdbc.getOptions().autoReconnect); + Assert.assertTrue(jdbc.getOptions().createDatabaseIfNotExist); + + Assert.assertTrue("root".equals(jdbc.getOptions().user)); + Assert.assertTrue("root".equals(jdbc.getUsername())); + + Assert.assertTrue("toto".equals(jdbc.getOptions().password)); + Assert.assertTrue("toto".equals(jdbc.getPassword())); + } + + @Test + public void testOptionParseSlash() throws Throwable { + JDBCUrl jdbc = JDBCUrl.parse("jdbc:mysql://127.0.0.1:3306/colleo?user=root&password=toto&localSocket=/var/run/mysqld/mysqld.sock"); + Assert.assertTrue("/var/run/mysqld/mysqld.sock".equals(jdbc.getOptions().localSocket)); + + Assert.assertTrue("root".equals(jdbc.getOptions().user)); + Assert.assertTrue("root".equals(jdbc.getUsername())); + + Assert.assertTrue("toto".equals(jdbc.getOptions().password)); + Assert.assertTrue("toto".equals(jdbc.getPassword())); + } + @Test + public void testOptionParseIntegerMinimum() throws Throwable { + JDBCUrl jdbc = JDBCUrl.parse("jdbc:mysql://localhost/test?user=root&autoReconnect=true&validConnectionTimeout=0&connectTimeout=5"); + Assert.assertTrue(jdbc.getOptions().connectTimeout == 5); + Assert.assertTrue(jdbc.getOptions().validConnectionTimeout == 0); + Assert.assertTrue(jdbc.getOptions().autoReconnect); + Assert.assertTrue("root".equals(jdbc.getOptions().user)); + } + + @Test(expected = IllegalArgumentException.class ) + public void testOptionParseIntegerNotPossible() throws Throwable { + JDBCUrl.parse("jdbc:mysql://localhost/test?user=root&autoReconnect=true&validConnectionTimeout=-2&connectTimeout=5"); + Assert.fail(); + } + + @Test() + public void testJDBCParserSimpleIPv4basic() { + String url = "jdbc:mysql://master:3306,slave1:3307,slave2:3308/database"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + } + @Test + public void testJDBCParserSimpleIPv4basicError() { + try { + JDBCUrl.parse(null); + Assert.fail(); + }catch (IllegalArgumentException e) { + Assert.assertTrue(true); + } + } + @Test + public void testJDBCParserSimpleIPv4basicwithoutDatabase() { + String url = "jdbc:mysql://master:3306,slave1:3307,slave2:3308/"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertNull(jdbcUrl.getDatabase()); + Assert.assertNull(jdbcUrl.getUsername()); + Assert.assertNull(jdbcUrl.getPassword()); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master", 3306).equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("slave1", 3307).equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave2", 3308).equals(jdbcUrl.getHostAddresses().get(2))); + } + + @Test + public void testJDBCParserSimpleIPv4Properties() { + String url = "jdbc:mysql://master:3306,slave1:3307,slave2:3308/database?autoReconnect=true"; + Properties prop = new Properties(); + prop.setProperty("user","greg"); + prop.setProperty("password","pass"); + + JDBCUrl jdbcUrl = JDBCUrl.parse(url, prop); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue("greg".equals(jdbcUrl.getUsername())); + Assert.assertTrue("pass".equals(jdbcUrl.getPassword())); + Assert.assertTrue(jdbcUrl.getOptions().autoReconnect); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master", 3306).equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("slave1", 3307).equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave2", 3308).equals(jdbcUrl.getHostAddresses().get(2))); + } + + @Test + public void testJDBCParserSimpleIPv4() { + String url = "jdbc:mysql://master:3306,slave1:3307,slave2:3308/database?user=greg&password=pass"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue("greg".equals(jdbcUrl.getUsername())); + Assert.assertTrue("pass".equals(jdbcUrl.getPassword())); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master", 3306).equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("slave1", 3307).equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave2", 3308).equals(jdbcUrl.getHostAddresses().get(2))); + } + + + @Test + public void testJDBCParserSimpleIPv6() { + String url = "jdbc:mysql://[2001:0660:7401:0200:0000:0000:0edf:bdd7]:3306,[2001:660:7401:200::edf:bdd7]:3307/database?user=greg&password=pass"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue("greg".equals(jdbcUrl.getUsername())); + Assert.assertTrue("pass".equals(jdbcUrl.getPassword())); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 2); + Assert.assertTrue(new HostAddress("2001:0660:7401:0200:0000:0000:0edf:bdd7", 3306).equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("2001:660:7401:200::edf:bdd7", 3307).equals(jdbcUrl.getHostAddresses().get(1))); + } + + + @Test + public void testJDBCParserParameter() { + String url = "jdbc:mysql://address=(type=master)(port=3306)(host=master1),address=(port=3307)(type=master)(host=master2),address=(type=slave)(host=slave1)(port=3308)/database?user=greg&password=pass"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue("greg".equals(jdbcUrl.getUsername())); + Assert.assertTrue("pass".equals(jdbcUrl.getPassword())); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master1", 3306, "master").equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("master2", 3307, "master").equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave1", 3308, "slave").equals(jdbcUrl.getHostAddresses().get(2))); + } + + @Test + public void testJDBCParserParameterError() { + try { + JDBCUrl.parse(null); + Assert.fail(); + }catch (IllegalArgumentException e) { + Assert.assertTrue(true); + } + } + + @Test + public void testJDBCParserParameterErrorEqual() { + String url = "jdbc:mysql://address=(type=)(port=3306)(host=master1),address=(port=3307)(type=master)(host=master2),address=(type=slave)(host=slave1)(port=3308)/database?user=greg&password=pass"; + try { + JDBCUrl.parse(null); + Assert.fail(); + }catch (IllegalArgumentException e) { + Assert.assertTrue(true); + } + } + + @Test + public void testJDBCParserHAModeNone() { + String url = "jdbc:mysql://localhost/database"; + JDBCUrl jdbc = JDBCUrl.parse(url); + Assert.assertTrue(jdbc.getHaMode().equals(UrlHAMode.NONE)); + } + + @Test + public void testJDBCParserHAModeLoadReplication() { + String url = "jdbc:mysql:replication://localhost/database"; + JDBCUrl jdbc = JDBCUrl.parse(url); + Assert.assertTrue(jdbc.getHaMode().equals(UrlHAMode.REPLICATION)); + } + + @Test + public void testJDBCParserReplicationParameter() { + String url = "jdbc:mysql:replication://address=(type=master)(port=3306)(host=master1),address=(port=3307)(type=master)(host=master2),address=(type=slave)(host=slave1)(port=3308)/database?user=greg&password=pass"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue("greg".equals(jdbcUrl.getUsername())); + Assert.assertTrue("pass".equals(jdbcUrl.getPassword())); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master1", 3306, "master").equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("master2", 3307, "master").equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave1", 3308, "slave").equals(jdbcUrl.getHostAddresses().get(2))); + } + + @Test + public void testJDBCParserReplicationParameterWithoutType() { + String url = "jdbc:mysql:replication://master1,slave1,slave2/database"; + JDBCUrl jdbcUrl = JDBCUrl.parse(url); + Assert.assertTrue("database".equals(jdbcUrl.getDatabase())); + Assert.assertTrue(jdbcUrl.getHostAddresses().size() == 3); + Assert.assertTrue(new HostAddress("master1", 3306, "master").equals(jdbcUrl.getHostAddresses().get(0))); + Assert.assertTrue(new HostAddress("slave1", 3306, "slave").equals(jdbcUrl.getHostAddresses().get(1))); + Assert.assertTrue(new HostAddress("slave2", 3306, "slave").equals(jdbcUrl.getHostAddresses().get(2))); + } + + @Test + public void testJDBCParserHAModeLoadAurora() { + String url = "jdbc:mysql:aurora://localhost/database"; + JDBCUrl jdbc = JDBCUrl.parse(url); + Assert.assertTrue(jdbc.getHaMode().equals(UrlHAMode.AURORA)); + } + +} diff --git a/src/test/java/org/mariadb/jdbc/MultiTest.java b/src/test/java/org/mariadb/jdbc/MultiTest.java index f41b83d9f..54dac43a9 100644 --- a/src/test/java/org/mariadb/jdbc/MultiTest.java +++ b/src/test/java/org/mariadb/jdbc/MultiTest.java @@ -7,14 +7,14 @@ import java.util.logging.Level; import java.util.logging.Logger; -import junit.framework.Assert; +import org.junit.Assert; import org.mariadb.jdbc.internal.common.packet.PacketOutputStream; import static org.junit.Assert.*; public class MultiTest extends BaseTest { - private static Connection connection; + private static Connection connectionMulti; public MultiTest() throws SQLException { } @@ -23,8 +23,8 @@ public MultiTest() throws SQLException { public static void beforeClassMultiTest() throws SQLException { BaseTest baseTest = new BaseTest(); baseTest.setConnection("&allowMultiQueries=true"); - connection = baseTest.connection; - Statement st = connection.createStatement(); + connectionMulti = baseTest.connection; + Statement st = connectionMulti.createStatement(); st.executeUpdate("drop table if exists t1"); st.executeUpdate("drop table if exists t2"); st.executeUpdate("drop table if exists t3"); @@ -46,7 +46,7 @@ public static void beforeClassMultiTest() throws SQLException { @AfterClass public static void afterClass() throws SQLException { try { - Statement st = connection.createStatement(); + Statement st = connectionMulti.createStatement(); st.executeUpdate("drop table if exists t1"); st.executeUpdate("drop table if exists t2"); st.executeUpdate("drop table if exists t3"); @@ -55,7 +55,7 @@ public static void afterClass() throws SQLException { // eat } finally { try { - connection.close(); + connectionMulti.close(); } catch (Exception e) { } @@ -65,7 +65,8 @@ public static void afterClass() throws SQLException { @Test public void basicTest() throws SQLException { - Statement statement = connection.createStatement(); + log.fine("basicTest begin"); + Statement statement = connectionMulti.createStatement(); ResultSet rs = statement.executeQuery("select * from t1;select * from t2;"); int count = 0; while (rs.next()) { @@ -80,11 +81,13 @@ public void basicTest() throws SQLException { } assertTrue(count > 0); assertFalse(statement.getMoreResults()); + log.fine("basicTest end"); } @Test public void updateTest() throws SQLException { - Statement statement = connection.createStatement(); + log.fine("updateTest begin"); + Statement statement = connectionMulti.createStatement(); statement.execute("update t5 set test='a " + System.currentTimeMillis() + "' where id = 2;select * from t2;"); int updateNb = statement.getUpdateCount(); log.fine("statement.getUpdateCount() " + updateNb); @@ -97,11 +100,13 @@ public void updateTest() throws SQLException { } assertTrue(count > 0); assertFalse(statement.getMoreResults()); + log.fine("updateTest end"); } @Test public void updateTest2() throws SQLException { - Statement statement = connection.createStatement(); + log.fine("updateTest2 begin"); + Statement statement = connectionMulti.createStatement(); statement.execute("select * from t2;update t5 set test='a " + System.currentTimeMillis() + "' where id = 2;"); ResultSet rs = statement.getResultSet(); int count = 0; @@ -114,11 +119,13 @@ public void updateTest2() throws SQLException { int updateNb = statement.getUpdateCount(); log.fine("statement.getUpdateCount() " + updateNb); assertEquals(2, updateNb); + log.fine("updateTest2 end"); } @Test public void selectTest() throws SQLException { - Statement statement = connection.createStatement(); + log.fine("selectTest begin"); + Statement statement = connectionMulti.createStatement(); statement.execute("select * from t2;select * from t1;"); ResultSet rs = statement.getResultSet(); int count = 0; @@ -132,11 +139,13 @@ public void selectTest() throws SQLException { count++; } assertTrue(count > 0); + log.fine("selectTest end"); } @Test public void setMaxRowsMulti() throws Exception { - Statement st = connection.createStatement(); + log.fine("setMaxRowsMulti begin"); + Statement st = connectionMulti.createStatement(); assertEquals(0, st.getMaxRows()); st.setMaxRows(1); @@ -163,6 +172,7 @@ public void setMaxRowsMulti() throws Exception { } rs.close(); assertEquals(1, cnt); + log.fine("setMaxRowsMulti end"); } /** @@ -172,20 +182,26 @@ public void setMaxRowsMulti() throws Exception { */ @Test public void rewriteBatchedStatementsDisabledInsertionTest() throws SQLException { + log.fine("rewriteBatchedStatementsDisabledInsertionTest begin"); verifyInsertBehaviorBasedOnRewriteBatchedStatements(Boolean.FALSE, 3000); + log.fine("rewriteBatchedStatementsDisabledInsertionTest end"); } @Test public void rewriteBatchedStatementsEnabledInsertionTest() throws SQLException { + log.fine("rewriteBatchedStatementsEnabledInsertionTest begin"); //On batch mode, single insert query will be sent to MariaDB server. verifyInsertBehaviorBasedOnRewriteBatchedStatements(Boolean.TRUE, 1); + log.fine("rewriteBatchedStatementsEnabledInsertionTest end"); } private void verifyInsertBehaviorBasedOnRewriteBatchedStatements(Boolean rewriteBatchedStatements, int totalInsertCommands) throws SQLException { Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", rewriteBatchedStatements.toString()); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); verifyInsertCount(tmpConnection, 0); int cycles = 3000; tmpConnection.createStatement().execute("TRUNCATE t1"); @@ -231,11 +247,14 @@ private int retrieveSessionVariableFromServer(Connection tmpConnection, String v */ @Test public void rewriteBatchedStatementsSemicolon() throws SQLException { + log.fine("rewriteBatchedStatementsSemicolon begin"); // set the rewrite batch statements parameter Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", "true"); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); tmpConnection.createStatement().execute("TRUNCATE t3"); PreparedStatement sqlInsert = tmpConnection.prepareStatement("INSERT INTO t3 (message) VALUES (?)"); @@ -274,7 +293,8 @@ public void rewriteBatchedStatementsSemicolon() throws SQLException { verifyInsertCount(tmpConnection, 5); tmpConnection.commit(); } finally { - tmpConnection.close(); + log.fine("rewriteBatchedStatementsSemicolon end"); + if (tmpConnection != null) tmpConnection.close(); } } @@ -299,11 +319,14 @@ private PreparedStatement prepareStatementBatch(Connection tmpConnection, int si */ @Test public void rewriteBatchedStatementsUpdateTest() throws SQLException { + log.fine("rewriteBatchedStatementsUpdateTest begin"); // set the rewrite batch statements parameter Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", "true"); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); tmpConnection.setClientInfo(props); verifyUpdateCount(tmpConnection, 0); tmpConnection.createStatement().execute("TRUNCATE t1"); @@ -326,7 +349,8 @@ public void rewriteBatchedStatementsUpdateTest() throws SQLException { verifyUpdateCount(tmpConnection, cycles); //1000 update commande launched assertEquals(cycles * 2, totalUpdates); // 2000 rows updates } finally { - tmpConnection.close(); + log.fine("rewriteBatchedStatementsUpdateTest end"); + if (tmpConnection != null) tmpConnection.close(); } } @@ -336,11 +360,14 @@ public void rewriteBatchedStatementsUpdateTest() throws SQLException { */ @Test public void testMultipleExecuteBatch() throws SQLException { + log.fine("testMultipleExecuteBatch begin"); // set the rewrite batch statements parameter Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", "true"); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); tmpConnection.setClientInfo(props); verifyUpdateCount(tmpConnection, 0); tmpConnection.createStatement().execute("TRUNCATE t1"); @@ -364,16 +391,20 @@ public void testMultipleExecuteBatch() throws SQLException { updateCounts = preparedStatement.executeBatch(); assertEquals(1, updateCounts.length); } finally { - tmpConnection.close(); + log.fine("testMultipleExecuteBatch end"); + if (tmpConnection != null) tmpConnection.close(); } } @Test public void rewriteBatchedStatementsInsertWithDuplicateRecordsTest() throws SQLException { + log.fine("rewriteBatchedStatementsInsertWithDuplicateRecordsTest begin"); Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", "true"); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); verifyInsertCount(tmpConnection, 0); tmpConnection.createStatement().execute("TRUNCATE reWriteDuplicateTestTable"); Statement statement = tmpConnection.createStatement(); @@ -391,16 +422,20 @@ public void rewriteBatchedStatementsInsertWithDuplicateRecordsTest() throws SQLE verifyInsertCount(tmpConnection, 1); verifyUpdateCount(tmpConnection, 0); } finally { - tmpConnection.close(); + log.fine("rewriteBatchedStatementsInsertWithDuplicateRecordsTest end"); + if (tmpConnection != null) tmpConnection.close(); } } @Test public void updateCountTest() throws SQLException { + log.fine("updateCountTest begin"); Properties props = new Properties(); props.setProperty("rewriteBatchedStatements", "true"); - Connection tmpConnection = openNewConnection(connURI, props); + props.setProperty("allowMultiQueries", "true"); + Connection tmpConnection = null; try { + tmpConnection = openNewConnection(connURI, props); PreparedStatement sqlInsert = tmpConnection.prepareStatement("INSERT IGNORE INTO t4 (id,test) VALUES (?,?)"); sqlInsert.setInt(1, 1); sqlInsert.setString(2, "value1"); @@ -442,7 +477,8 @@ public void updateCountTest() throws SQLException { Assert.assertEquals(0, updateCounts[1]); Assert.assertEquals(2, updateCounts[2]); } finally { - tmpConnection.close(); + log.fine("updateCountTest end"); + if (tmpConnection != null) tmpConnection.close(); } } diff --git a/src/test/java/org/mariadb/jdbc/MySQLDriverTest.java b/src/test/java/org/mariadb/jdbc/MySQLDriverTest.java index a8cc8d5a1..dab532a1e 100644 --- a/src/test/java/org/mariadb/jdbc/MySQLDriverTest.java +++ b/src/test/java/org/mariadb/jdbc/MySQLDriverTest.java @@ -17,8 +17,8 @@ import java.util.Arrays; import java.util.Properties; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; @@ -615,7 +615,7 @@ public void executeStatementAfterConnectionClose() throws Exception { try { st.execute("select 1"); } catch (SQLException sqle) { - assertTrue(sqle.getMessage().contains("closed connection")); + assertTrue(sqle.getMessage().contains("execute() is called on closed connection")); } } @@ -647,7 +647,7 @@ public void connectToDbWithDashInName() throws Exception { public void connectCreateDB() throws Exception { String oldParams = parameters; String oldDb = database; - setParameters("&createDB=true"); + setParameters("&createDatabaseIfNotExist=true"); setDatabase("no-such-db"); try { assertEquals("no-such-db",connection.getCatalog()); diff --git a/src/test/java/org/mariadb/jdbc/ParserTest.java b/src/test/java/org/mariadb/jdbc/ParserTest.java index 26e293809..5a3927111 100644 --- a/src/test/java/org/mariadb/jdbc/ParserTest.java +++ b/src/test/java/org/mariadb/jdbc/ParserTest.java @@ -1,15 +1,25 @@ package org.mariadb.jdbc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.sql.ResultSet; +import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.Statement; +import java.util.Properties; +import org.junit.Assert.*; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mariadb.jdbc.internal.common.Options; +import org.mariadb.jdbc.internal.common.UrlHAMode; +import org.mariadb.jdbc.internal.common.query.IllegalParameterException; +import org.mariadb.jdbc.internal.mysql.Protocol; public class ParserTest extends BaseTest { private Statement statement; @@ -29,6 +39,25 @@ public void cleanup() { } } + @Test + public void addProperties() throws Exception { + Field field = MySQLConnection.class.getDeclaredField("options"); + field.setAccessible(true); + Options options = (Options) field.get(connection); + assertFalse(options.useSSL); + connection.setClientInfo("useSSL", "true"); + + options = (Options) field.get(connection); + assertTrue(options.useSSL); + + Properties prop = new Properties(); + prop.put("autoReconnect", "true"); + prop.put("useSSL", "false"); + connection.setClientInfo(prop); + assertFalse(options.useSSL); + assertTrue(options.autoReconnect); + } + @Test public void libreOfficeBase() { String sql; diff --git a/src/test/java/org/mariadb/jdbc/PooledConnectionTest.java b/src/test/java/org/mariadb/jdbc/PooledConnectionTest.java index 98a3e2efe..7fe792199 100644 --- a/src/test/java/org/mariadb/jdbc/PooledConnectionTest.java +++ b/src/test/java/org/mariadb/jdbc/PooledConnectionTest.java @@ -1,15 +1,12 @@ package org.mariadb.jdbc; +import org.junit.Assert; import org.junit.Test; import javax.sql.*; import java.sql.*; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -class MyEventListener implements ConnectionEventListener,StatementEventListener -{ +class MyEventListener implements ConnectionEventListener,StatementEventListener { public SQLException sqlException; public boolean closed; public boolean connectionErrorOccured; @@ -51,7 +48,7 @@ public void testPooledConnectionClosed() throws Exception { MyEventListener listener = new MyEventListener(); pc.addConnectionEventListener(listener); c.close(); - assertTrue(listener.closed); + Assert.assertTrue(listener.closed); /* Verify physical connection is still ok */ c.createStatement().execute("select 1"); @@ -60,7 +57,7 @@ public void testPooledConnectionClosed() throws Exception { /* Now verify physical connection is gone */ try { c.createStatement().execute("select 1"); - assertFalse("should never get there", true); + Assert.assertFalse("should never get there", true); } catch(Exception e) { } @@ -85,12 +82,8 @@ public void testPooledConnectionException() throws Exception { /* Try to read after server side closed the connection */ try { c.createStatement().execute("SELECT 1"); - - assertTrue("should never get there", false); - } - catch (Exception e) { - /* Check that listener was actually called*/ - assertTrue(listener.sqlException instanceof SQLNonTransientConnectionException); + Assert.assertTrue("should never get there", false); + } catch (SQLException e) { } pc.close(); //assertTrue(listener.closed); @@ -108,13 +101,13 @@ public void testPooledConnectionStatementError() throws Exception PreparedStatement ps = c.prepareStatement("zzzz"); try { ps.execute(); - assertTrue("should never get there", false); + Assert.assertTrue("should never get there", false); } catch(Exception e) { - assertTrue(listener.statementErrorOccured && listener.sqlException instanceof SQLSyntaxErrorException); + Assert.assertTrue(listener.statementErrorOccured && listener.sqlException instanceof SQLSyntaxErrorException); } ps.close(); - assertTrue(listener.statementClosed); + Assert.assertTrue(listener.statementClosed); pc.close(); } } diff --git a/src/test/java/org/mariadb/jdbc/ResultSetMetaDataTest.java b/src/test/java/org/mariadb/jdbc/ResultSetMetaDataTest.java index 6bcebad5b..83a0d2840 100644 --- a/src/test/java/org/mariadb/jdbc/ResultSetMetaDataTest.java +++ b/src/test/java/org/mariadb/jdbc/ResultSetMetaDataTest.java @@ -7,7 +7,7 @@ import java.util.logging.Level; import java.util.logging.Logger; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class ResultSetMetaDataTest extends BaseTest { static { diff --git a/src/test/java/org/mariadb/jdbc/TimeoutTest.java b/src/test/java/org/mariadb/jdbc/TimeoutTest.java index cd181e709..797840223 100644 --- a/src/test/java/org/mariadb/jdbc/TimeoutTest.java +++ b/src/test/java/org/mariadb/jdbc/TimeoutTest.java @@ -11,6 +11,7 @@ import java.sql.Statement; import java.util.Random; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -84,50 +85,42 @@ public void socketTimeoutTest() throws SQLException { ps = connection.prepareStatement("SELECT sleep(1)"); // a timeout should occur here - try - { + try { rs = ps.executeQuery(); + Assert.fail(); } catch (SQLException e) { // check that it's a timeout that occurs if (e.getMessage().contains("timed out")) exceptionCount++; } - - ps = connection.prepareStatement("SELECT 2"); - // connection should be closed here, exception expected - try - { - rs = ps.executeQuery(); - rs.next(); - } catch (SQLException e) { - // check that it's a execute "called on closed" exception that occurs - if (e.getMessage().contains("called on closed")) - exceptionCount++; + try { + ps = connection.prepareStatement("SELECT 2"); + ps.execute(); + Assert.fail(); + } catch (Exception e){ + } - // the connection should be closed + // the connection should be closed assertTrue(connection.isClosed()); - // there should have been two exceptions - assertTrue(exceptionCount == 2); } @Test public void waitTimeoutStatementTest() throws SQLException, InterruptedException { Statement statement = connection.createStatement(); statement.execute("set session wait_timeout=1"); - Thread.sleep(3000); // Wait for the server to kill the connection + Thread.sleep(2000); // Wait for the server to kill the connection logInfo(connection.toString()); // here a SQLNonTransientConnectionException is expected // "Could not read resultset: unexpected end of stream, ..." - try - { + try { statement.execute("SELECT 1"); + Assert.fail(); } catch (SQLException e) { - // verify that the correct type of exception is thrown - assertTrue(e.getMessage().contains("Could not read resultset")); + } statement.close(); @@ -147,13 +140,10 @@ public void waitTimeoutResultSetTest() throws SQLException, InterruptedException // here a SQLNonTransientConnectionException is expected // "Could not read resultset: unexpected end of stream, ..." - try - { + try { rs = stmt.executeQuery("SELECT 2"); rs.next(); } catch (SQLException e) { - // verify that the correct type of exception is thrown - assertTrue(e.getMessage().contains("Could not read resultset")); } } diff --git a/src/test/java/org/mariadb/jdbc/TimezoneDaylightSavingTimeTest.java b/src/test/java/org/mariadb/jdbc/TimezoneDaylightSavingTimeTest.java index 981fd564e..19b4ec749 100644 --- a/src/test/java/org/mariadb/jdbc/TimezoneDaylightSavingTimeTest.java +++ b/src/test/java/org/mariadb/jdbc/TimezoneDaylightSavingTimeTest.java @@ -56,9 +56,9 @@ public void setUp() throws SQLException { @After public void tearDown() { //Reset the FORMAT locate so other test cases are not disturbed. - Locale.setDefault(previousFormatLocale); + if (previousFormatLocale!=null) Locale.setDefault(previousFormatLocale); //Reset the timezone so so other test cases are not disturbed. - TimeZone.setDefault(previousTimeZone); + if (previousTimeZone != null ) TimeZone.setDefault(previousTimeZone); } diff --git a/src/test/java/org/mariadb/jdbc/UnicodeTest.java b/src/test/java/org/mariadb/jdbc/UnicodeTest.java index 2c595b949..fdc6136f2 100644 --- a/src/test/java/org/mariadb/jdbc/UnicodeTest.java +++ b/src/test/java/org/mariadb/jdbc/UnicodeTest.java @@ -6,7 +6,7 @@ import java.util.logging.Logger; import java.util.logging.Level; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class UnicodeTest extends BaseTest { diff --git a/src/test/java/org/mariadb/jdbc/UtilTest.java b/src/test/java/org/mariadb/jdbc/UtilTest.java index bf17a25d2..08c00e0b7 100644 --- a/src/test/java/org/mariadb/jdbc/UtilTest.java +++ b/src/test/java/org/mariadb/jdbc/UtilTest.java @@ -5,7 +5,7 @@ import java.sql.SQLException; -import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertEquals; public class UtilTest { diff --git a/src/test/java/org/mariadb/jdbc/XA.java b/src/test/java/org/mariadb/jdbc/XA.java index 05bbfe9e8..749f89917 100644 --- a/src/test/java/org/mariadb/jdbc/XA.java +++ b/src/test/java/org/mariadb/jdbc/XA.java @@ -1,9 +1,9 @@ package org.mariadb.jdbc; -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import java.sql.Connection; import java.sql.ResultSet; diff --git a/src/test/java/org/mariadb/jdbc/failover/AuroraFailoverTest.java b/src/test/java/org/mariadb/jdbc/failover/AuroraFailoverTest.java new file mode 100644 index 000000000..0754360df --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/AuroraFailoverTest.java @@ -0,0 +1,700 @@ +package org.mariadb.jdbc.failover; + +import org.junit.*; +import org.junit.Test; +import org.mariadb.jdbc.internal.mysql.FailoverProxy; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertTrue; + +public class AuroraFailoverTest extends BaseMultiHostTest { + private Connection connection; + private long testBeginTime; + + @Before + public void init() throws SQLException { + initialUrl = initialAuroraUrl; + proxyUrl = proxyAuroraUrl; + currentType = TestType.AURORA; + Assume.assumeTrue(initialAuroraUrl != null); + connection = null; + testBeginTime = System.currentTimeMillis(); + } + + @After + public void after() throws SQLException { + assureProxy(); + if (connection != null) { + connection.close(); + assureBlackList(connection); + } + log.fine("test time : " + (System.currentTimeMillis() - testBeginTime) + "ms"); + } + + @Test + public void testWriteOnMaster() throws SQLException { + connection = getNewConnection(false); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists multinode"); + stmt.execute("create table multinode (id int not null primary key auto_increment, test VARCHAR(10))"); + } + + @Test + public void testErrorWriteOnReplica() throws SQLException { + connection = getNewConnection(false); + connection.setReadOnly(true); + Statement stmt = connection.createStatement(); + Assert.assertTrue(connection.isReadOnly()); + try { + stmt.execute("drop table if exists multinode4"); + log.severe("ERROR - > must not be able to write on slave --> check if you database is start with --read-only"); + Assert.fail(); + } catch (SQLException e) { + } + } + + @Test + public void testReplication() throws SQLException, InterruptedException { + connection = getNewConnection(false); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists multinodeReadSlave"); + stmt.execute("create table multinodeReadSlave (id int not null primary key auto_increment, test VARCHAR(10))"); + + //wait to be sure slave have replicate data + Thread.sleep(200); + + connection.setReadOnly(true); + + ResultSet rs = stmt.executeQuery("Select count(*) from multinodeReadSlave"); + Assert.assertTrue(rs.next()); + } + + + @Test + public void randomConnection() throws Throwable { + Map connectionMap = new HashMap(); + int masterId = -1; + for (int i = 0; i < 20; i++) { + connection = getNewConnection(false); + int serverId = getServerId(connection); + log.fine("master server found " + serverId); + if (i > 0) Assert.assertTrue(masterId == serverId); + masterId = serverId; + connection.setReadOnly(true); + int replicaId = getServerId(connection); + log.fine("++++++++++++slave server found " + replicaId); + MutableInt count = connectionMap.get(String.valueOf(replicaId)); + if (count == null) { + connectionMap.put(String.valueOf(replicaId), new MutableInt()); + } else { + count.increment(); + } + connection.close(); + } + + Assert.assertTrue(connectionMap.size() >= 2); + for (String key : connectionMap.keySet()) { + Integer connectionCount = connectionMap.get(key).get(); + log.fine(" ++++ Server " + key + " : " + connectionCount + " connections "); + Assert.assertTrue(connectionCount > 1); + } + log.fine("randomConnection OK"); + } + + + @Test + public void failoverSlaveToMaster() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + Assert.assertFalse(masterServerId == slaveServerId); + stopProxy(slaveServerId); + connection.createStatement().execute("SELECT 1"); + int currentServerId = getServerId(connection); + + log.fine("masterServerId = " + masterServerId + "/currentServerId = " + currentServerId); + Assert.assertTrue(masterServerId == currentServerId); + + Assert.assertFalse(connection.isReadOnly()); + } + + + @Test + public void failoverSlaveToMasterFail() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=1", true); + int masterServerId = getServerId(connection); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + Assert.assertTrue(slaveServerId != masterServerId); + + connection.setCatalog("mysql"); //to be sure there will be a query, and so an error when switching connection + stopProxy(masterServerId); + try { + //must not throw error until there is a query + connection.setReadOnly(false); + Assert.fail(); + } catch (SQLException e) { + } + } + + @Test + public void pingReconnectAfterRestart() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=1&failOnReadOnly=false&queriesBeforeRetryMaster=50000", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + long stoppedTime = System.currentTimeMillis(); + try { + st.execute("SELECT 1"); + } catch (SQLException e) { + } + restartProxy(masterServerId); + long restartTime = System.currentTimeMillis(); + + boolean loop = true; + while (loop) { + if (!connection.isClosed()) { + log.fine("reconnection with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + loop = false; + } + if (System.currentTimeMillis() - restartTime > 15 * 1000) Assert.fail(); + Thread.sleep(250); + } + } + + + @Test + public void failoverDuringMasterSetReadOnly() throws Throwable { + int masterServerId = -1; + connection = getNewConnection("&retriesAllDown=1", true); + masterServerId = getServerId(connection); + + stopProxy(masterServerId); + + connection.setReadOnly(true); + + int slaveServerId = getServerId(connection); + + Assert.assertFalse(slaveServerId == masterServerId); + Assert.assertTrue(connection.isReadOnly()); + } + + @Test + public void failoverDuringSlaveSetReadOnly() throws Throwable { + connection = getNewConnection(true); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + + stopProxy(slaveServerId, 2000); + + connection.setReadOnly(false); + + int masterServerId = getServerId(connection); + + Assert.assertFalse(slaveServerId == masterServerId); + Assert.assertFalse(connection.isReadOnly()); + } + + @Test() + public void failoverSlaveAndMasterWithoutAutoConnect() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + connection.setReadOnly(true); + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + + try { + connection.createStatement().executeQuery("SELECT CONNECTION_ID()"); + } catch (SQLException e) { + Assert.fail(); + } + } + + @Test + public void reconnectSlaveAndMasterWithAutoConnect() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true", true); + + //search actual server_id for master and slave + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + + connection.setReadOnly(true); + + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + //must reconnect to the second slave without error + connection.createStatement().execute("SELECT 1"); + int currentSlaveId = getServerId(connection); + log.fine("currentSlaveId server_id = " + currentSlaveId); + Assert.assertTrue(currentSlaveId != firstSlaveId); + Assert.assertTrue(currentSlaveId != masterServerId); + } + + + @Test + public void failOnSlaveAndMasterWithAutoConnect() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true&failOnReadOnly=true", true); + + //search actual server_id for master and slave + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + + connection.setReadOnly(true); + + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + //must reconnect to the second slave without error + connection.createStatement().execute("SELECT 1"); + int currentSlaveId = getServerId(connection); + log.fine("currentSlaveId server_id = " + currentSlaveId); + Assert.assertTrue(currentSlaveId != firstSlaveId); + Assert.assertTrue(currentSlaveId != masterServerId); + } + + + @Test + public void failoverMasterWithAutoConnect() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true", true); + int masterServerId = getServerId(connection); + + stopProxy(masterServerId, 250); + //with autoreconnect, the connection must reconnect automatically + int currentServerId = getServerId(connection); + + Assert.assertTrue(currentServerId == masterServerId); + Assert.assertFalse(connection.isReadOnly()); + } + + @Test + public void checkReconnectionToMasterAfterQueryNumber() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=3000&queriesBeforeRetryMaster=10&failOnReadOnly=true", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + st.execute("SELECT 1"); + } catch (SQLException e) { + Assert.fail(); + } + Assert.assertTrue(connection.isReadOnly()); + + restartProxy(masterServerId); + long stoppedTime = System.currentTimeMillis(); + + //not in autoreconnect mode, so must wait for query more than queriesBeforeRetryMaster + for (int i = 1; i < 10; i++) { + try { + st.execute("SELECT 1"); + log.fine("i=" + i); + Assert.assertTrue(connection.isReadOnly()); + } catch (SQLException e) { + Assert.fail(); + } + } + + boolean loop = true; + while (loop) { + try { + Thread.sleep(250); + st.execute("SELECT 1"); + if (!connection.isReadOnly()) { + log.fine("reconnection with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + loop = false; + } + } catch (SQLException e) { + log.fine("not reconnected ... "); + } + if (System.currentTimeMillis() - stoppedTime > 20 * 1000) Assert.fail(); + } + } + + @Test + public void reconnectMasterAfterFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + //if super user can write on slave + Assume.assumeTrue(!hasSuperPrivilege(connection, "reconnectMasterAfterFailover")); + Statement st = connection.createStatement(); + st.execute("drop table if exists multinode2"); + st.execute("create table multinode2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + st.execute("insert into multinode2 (id, amount) VALUE (1 , 100)"); + + int masterServerId = getServerId(connection); + long stopTime = System.currentTimeMillis(); + stopProxy(masterServerId, 10000); + try { + st.execute("insert into multinode2 (id, amount) VALUE (2 , 100)"); + Assert.assertTrue(System.currentTimeMillis() - stopTime > 10); + Assert.assertTrue(System.currentTimeMillis() - stopTime < 20); + } catch (SQLException e) { + } + } + + @Test + public void writeToSlaveAfterFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + //if super user can write on slave + Assume.assumeTrue(!hasSuperPrivilege(connection, "writeToSlaveAfterFailover")); + Statement st = connection.createStatement(); + st.execute("drop table if exists multinode2"); + st.execute("create table multinode2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + st.execute("insert into multinode2 (id, amount) VALUE (1 , 100)"); + + int masterServerId = getServerId(connection); + + stopProxy(masterServerId); + try { + st.execute("insert into multinode2 (id, amount) VALUE (2 , 100)"); + Assert.fail(); + } catch (SQLException e) { + } + } + + @Test + public void checkBackOnMasterOnSlaveFail() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=1&failOnReadOnly=true", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + try { + st.execute("SELECT 1"); + Assert.assertTrue(connection.isReadOnly()); + } catch (SQLException e) { + Assert.fail(); + } + + long stoppedTime = System.currentTimeMillis(); + restartProxy(masterServerId); + boolean loop = true; + while (loop) { + Thread.sleep(250); + try { + if (!connection.isReadOnly()) { + log.fine("reconnection to master with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + loop = false; + } + } catch (SQLException e) { + } + if (System.currentTimeMillis() - stoppedTime > 15 * 1000) Assert.fail(); + } + } + + @Test() + public void checkNoSwitchConnectionDuringTransaction() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true", false); + Statement st = connection.createStatement(); + + st.execute("drop table if exists multinodeTransaction2"); + st.execute("create table multinodeTransaction2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + connection.setAutoCommit(false); + st.execute("insert into multinodeTransaction2 (id, amount) VALUE (1 , 100)"); + + try { + //in transaction, so must trow an error + connection.setReadOnly(true); + Assert.fail(); + } catch (SQLException e) { + } + } + + @Test + public void failoverMasterWithAutoConnectAndTransaction() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + Statement st = connection.createStatement(); + + int masterServerId = getServerId(connection); + st.execute("drop table if exists multinodeTransaction"); + st.execute("create table multinodeTransaction (id int not null primary key , amount int not null) ENGINE = InnoDB"); + connection.setAutoCommit(false); + st.execute("insert into multinodeTransaction (id, amount) VALUE (1 , 100)"); + stopProxy(masterServerId); + Assert.assertTrue(inTransaction(connection)); + try { + // will to execute the query. if there is a connection error, try a ping, if ok, good, query relaunched. If not good, transaction is considered be lost + st.execute("insert into multinodeTransaction (id, amount) VALUE (2 , 10)"); + Assert.fail(); + } catch (SQLException e) { + log.finest("normal error : " + e.getMessage()); + } + restartProxy(masterServerId); + try { + st = connection.createStatement(); + st.execute("insert into multinodeTransaction (id, amount) VALUE (2 , 10)"); + } catch (SQLException e) { + e.printStackTrace(); + Assert.fail(); + } + } + + @Test + public void testFailMaster() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true&failOnReadOnly=false", true); + Statement stmt = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + long stopTime = System.currentTimeMillis(); + try { + stmt.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + //normal error + } + Assert.assertTrue(!connection.isReadOnly()); + Assert.assertTrue(System.currentTimeMillis() - stopTime < 20 * 1000); + } + + @Test + public void testAutoReconnectMasterFailSlave() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true&failOnReadOnly=true", true); + Statement stmt = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + stmt.execute("SELECT 1"); + } catch (SQLException e) { + Assert.fail(); + } + Assert.assertTrue(connection.isReadOnly()); + } + + class MutableInt { + int value = 1; // note that we start at 1 since we're counting + + public void increment() { + ++value; + } + + public int get() { + return value; + } + } + + + + /** + * CONJ-79 + * + * @throws SQLException + */ + @Test + public void socketTimeoutTest() throws SQLException { + int exceptionCount = 0; + // set a short connection timeout + connection = getNewConnection("&socketTimeout=4000", false); + + PreparedStatement ps = connection.prepareStatement("SELECT 1"); + ResultSet rs = ps.executeQuery(); + rs.next(); + + // wait for the connection to time out + ps = connection.prepareStatement("SELECT sleep(5)"); + + // a timeout should occur here + try { + rs = ps.executeQuery(); + Assert.fail(); + } catch (SQLException e) { + // check that it's a timeout that occurs + Assert.assertTrue(e.getMessage().contains("timed out")); + } + try { + ps = connection.prepareStatement("SELECT 2"); + ps.execute(); + } catch (Exception e){ + Assert.fail(); + } + + try { + rs = ps.executeQuery(); + } catch (SQLException e) { + Assert.fail(); + } + + // the connection should not be closed + assertTrue(!connection.isClosed()); + } +// @Test +// public void testFailoverMaster() throws Throwable { +// connection = getNewConnection("&validConnectionTimeout=1&secondsBeforeRetryMaster=1&autoReconnect=true", false); +// Statement stmt = connection.createStatement(); +// ResultSet rs; +// stmt.execute("ALTER SYSTEM SIMULATE 100 PERCENT DISK FAILURE FOR INTERVAL 60 SECOND"); +// int initMaster = getServerId(connection); +// log.fine("master is " + initMaster); +// FailoverProxy proxy = getProtocolFromConnection(connection).getProxy(); +// log.fine("lock : "+proxy.lock.getReadHoldCount() + " " + proxy.lock.getWriteHoldCount()); +// long failInitTime = 0; +// long launchInit = System.currentTimeMillis(); +// while (System.currentTimeMillis() - launchInit < 120000) { +// try { +// Thread.sleep(2000); +// if (stmt.isClosed()) { +// stmt = connection.createStatement(); +// } +// rs = stmt.executeQuery("show global variables like 'innodb_read_only'"); +// +// rs.next(); +// +// if (failInitTime != 0 && "OFF".equals(rs.getString(2))) { +// long endFailover = System.currentTimeMillis() - launchInit; +// Assert.assertTrue(initMaster != getServerId(connection)); +// log.fine("End failover after " + endFailover + " new master is " + getServerId(connection)); +// //wait 15s for others tests may not be disturb by replica restart +// Thread.sleep(15000); +// return; +// } +// } catch (SQLException e) { +// log.fine("error : " + e.getMessage()); +// if (failInitTime == 0) { +// failInitTime = System.currentTimeMillis(); +// log.fine("start failover master was " + getServerId(connection)); +// } +// } +// } +// Assert.fail(); +// } +// +// @Test +// public void testFailoverMasterPing() throws Throwable { +// connection = getNewConnection("&failOnReadOnly=false&validConnectionTimeout=2&autoReconnect=true", false); +// Statement stmt = connection.createStatement(); +// stmt.execute("ALTER SYSTEM SIMULATE 100 PERCENT DISK FAILURE FOR INTERVAL 35 SECOND"); +// int initMaster = getServerId(connection); +// log.fine("master is " + initMaster); +// long launchInit = System.currentTimeMillis(); +// while (System.currentTimeMillis() - launchInit < 180000) { +// Thread.sleep(250); +// int currentMaster = getServerId(connection); +// +// //master must change +// if (currentMaster != initMaster) { +// long endFailover = System.currentTimeMillis() - launchInit; +// log.fine("Master automatically change after failover after " + endFailover + "ms. new master is " + currentMaster); +// +// //wait 15s for others tests may not be disturb by replica restart +// Thread.sleep(15000); +// return; +// } else { +// log.fine("++++++++ping in testFailoverMasterPing "); +// } +// } +// Assert.fail(); +// } +// +// +// @Test +// public void FailoverWithAutoMasterSet () throws Throwable { +// connection = getNewConnection("&validConnectionTimeout=2&secondsBeforeRetryMaster=1", false); +// int initMaster = getServerId(connection); +// connection.setReadOnly(true); +// int connection1SlaveId = getServerId(connection); +// int connection2SlaveId = connection1SlaveId; +// FailoverProxy proxy = getProtocolFromConnection(connection).getProxy(); +// Connection connection2 = null; +// try { +// while (connection2SlaveId == connection1SlaveId) { +// connection2 = getNewConnection("&validConnectionTimeout=5&secondsBeforeRetryMaster=1", false); +// connection2.setReadOnly(true); +// connection2SlaveId = getServerId(connection2); +// if (connection2SlaveId == connection1SlaveId) connection2.close(); +// } +// +// log.fine("master is " + initMaster); +// log.fine("connection 1 slave is " + connection1SlaveId); +// log.fine("connection 2 slave is " + connection2SlaveId); +// +// connection.setReadOnly(false); +// connection2.setReadOnly(false); +// +// // now whe know the master and every slave. +// // one of those replica will become a master +// // the goal of this test is to check that he become silently master. +// Statement stmt = connection.createStatement(); +// stmt.execute("ALTER SYSTEM SIMULATE 100 PERCENT DISK FAILURE FOR INTERVAL 35 SECOND"); +// +// boolean validationConnection1 = false; +// boolean validationConnection2 = false; +// +// //this permit to launched a failover is less than 15s after and every second, ping will test that master is ok. +// long launchInit = System.currentTimeMillis(); +// while (System.currentTimeMillis() - launchInit < 180000) { +// Thread.sleep(250); +// +// int currentMaster1 = getServerId(connection); +// int currentMaster2 = getServerId(connection2); +// +// //master must change +// if (currentMaster1 != initMaster || currentMaster2 != initMaster) { +// //connection master 1 has changed +// if (!validationConnection1 && currentMaster1 != initMaster) { +// //master has changed +// if (currentMaster1 == connection1SlaveId) { +// //master was old replica -> check that old replica has changed +// try { +// connection.setReadOnly(true); +// int currentSlave = getServerId(connection); +// +// if (currentSlave != currentMaster1) { +// Thread.sleep(15000); //give time to detect salve too +// currentSlave = getServerId(connection); +// } +// +// Assert.assertTrue(currentSlave != currentMaster1); +// validationConnection1 = true; +// } catch (SQLException e) {} +// } else validationConnection1 = true; +// } +// +// //connection master 1 has changed +// if (!validationConnection2 && currentMaster2 != initMaster) { +// //master has changed +// if (currentMaster2 == connection2SlaveId) { +// //master was old replica -> check that old replica has changed +// try { +// connection2.setReadOnly(true); +// connection2.createStatement().execute("SELECT 1"); +// int currentSlave = getServerId(connection2); +// +// if (currentSlave != currentMaster2) { +// Thread.sleep(15000); //give time to detect salve too +// currentSlave = getServerId(connection2); +// } +// +// Assert.assertTrue(currentSlave != currentMaster2); +// validationConnection2 = true; +// } catch (SQLException e) {} +// } else validationConnection2 = true; +// } +// if (validationConnection1 && validationConnection2) { +// //wait 15s for others tests may not be disturb by replica restart +// log.finest("validated"); +// Thread.sleep(15000); +// return; +// } +// } +// } +// Assert.assertTrue(validationConnection1 && validationConnection2); +// } finally { +// connection2.close(); +// } +// } + +} diff --git a/src/test/java/org/mariadb/jdbc/failover/BaseMultiHostTest.java b/src/test/java/org/mariadb/jdbc/failover/BaseMultiHostTest.java new file mode 100644 index 000000000..53e4155a1 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/BaseMultiHostTest.java @@ -0,0 +1,240 @@ +package org.mariadb.jdbc.failover; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.MySQLConnection; +import org.mariadb.jdbc.internal.common.UrlHAMode; +import org.mariadb.jdbc.internal.mysql.*; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.sql.*; +import java.util.HashMap; +import java.util.List; +import java.util.logging.*; + +/** + * Base util class. + * For testing + * example mvn test -DdbUrl=jdbc:mysql://localhost:3306,localhost:3307/test?user=root -DlogLevel=FINEST + * + * specific parameters : + * defaultMultiHostUrl : + * + * + */ +@Ignore +public class BaseMultiHostTest { + protected static Logger log = Logger.getLogger("org.mariadb.jdbc"); + + protected static String initialGaleraUrl; + protected static String initialAuroraUrl; + protected static String initialReplicationUrl; + protected static String initialUrl; + + + protected static String proxyGaleraUrl; + protected static String proxyAuroraUrl; + protected static String proxyReplicationUrl; + protected static String proxyUrl; + + protected static String username; + private static String hostname; + public enum TestType { + AURORA, REPLICATION, GALERA, NONE + } + public TestType currentType; + + //hosts + private static HashMap proxySet = new HashMap<>(); + + @Rule + public TestRule watcher = new TestWatcher() { + protected void starting(Description description) { + log.fine("Starting test: " + description.getMethodName()); + } + + protected void finished(Description description) { + log.fine("finished test: " + description.getMethodName()); + } + }; + + @BeforeClass + public static void beforeClass() throws SQLException, IOException { + + initialUrl = System.getProperty("dbUrl"); + initialGaleraUrl = System.getProperty("defaultGaleraUrl"); + initialReplicationUrl = System.getProperty("defaultReplicationUrl"); + initialAuroraUrl = System.getProperty("defaultAuroraUrl"); + + if (initialUrl != null) proxyUrl=createProxies(initialUrl, TestType.NONE); + if (initialReplicationUrl != null) proxyReplicationUrl=createProxies(initialReplicationUrl, TestType.REPLICATION); + if (initialGaleraUrl != null) proxyGaleraUrl=createProxies(initialGaleraUrl, TestType.GALERA); + if (initialAuroraUrl != null) proxyAuroraUrl=createProxies(initialAuroraUrl, TestType.AURORA); + } + + public static boolean requireMinimumVersion(Connection connection, int major, int minor) throws SQLException { + DatabaseMetaData md = connection.getMetaData(); + int dbMajor = md.getDatabaseMajorVersion(); + int dbMinor = md.getDatabaseMinorVersion(); + return (dbMajor > major || + (dbMajor == major && dbMinor >= minor)); + } + + private static String createProxies(String tmpUrl, TestType proxyType) { + JDBCUrl tmpJdbcUrl = JDBCUrl.parse(tmpUrl); + TcpProxy[] tcpProxies = new TcpProxy[tmpJdbcUrl.getHostAddresses().size()]; + username = tmpJdbcUrl.getUsername(); + hostname = tmpJdbcUrl.getHostAddresses().get(0).host; + String sockethosts = ""; + HostAddress hostAddress; + for (int i=0;i localhost:" + tcpProxies[i].getLocalPort()); + sockethosts+=",address=(host=localhost)(port="+tcpProxies[i].getLocalPort()+")"+((hostAddress.type != null)?"(type="+hostAddress.type+")":""); + } catch (IOException e) { + e.printStackTrace(); + } + } + proxySet.put(proxyType, tcpProxies); + if (tmpJdbcUrl.getHaMode().equals(UrlHAMode.NONE)) { + return "jdbc:mysql://"+sockethosts.substring(1)+"/"+tmpUrl.split("/")[3]; + } else { + return "jdbc:mysql:"+tmpJdbcUrl.getHaMode().toString().toLowerCase()+"://"+sockethosts.substring(1)+"/"+tmpUrl.split("/")[3]; + } + + } + + + protected Connection getNewConnection() throws SQLException { + return getNewConnection(null, false); + } + + protected Connection getNewConnection(boolean proxy) throws SQLException { + return getNewConnection(null, proxy); + } + + protected Connection getNewConnection(String additionnalConnectionData, boolean proxy) throws SQLException { + return getNewConnection(additionnalConnectionData, proxy, false); + } + + protected Connection getNewConnection(String additionnalConnectionData, boolean proxy, boolean forceNewProxy) throws SQLException { + if (proxy) { + String tmpProxyUrl = proxyUrl; + if (forceNewProxy) { + tmpProxyUrl = createProxies(initialUrl, currentType); + } + if (additionnalConnectionData == null) { + return DriverManager.getConnection(tmpProxyUrl); + } else { + return DriverManager.getConnection(tmpProxyUrl + additionnalConnectionData); + } + } else { + if (additionnalConnectionData == null) { + return DriverManager.getConnection(initialUrl); + } else { + return DriverManager.getConnection(initialUrl + additionnalConnectionData); + } + } + } + + @AfterClass + public static void afterClass() throws SQLException { + if (proxySet !=null) { + for (TcpProxy[] tcpProxies : proxySet.values()) { + for (TcpProxy tcpProxy : tcpProxies) { + try { + tcpProxy.stop(); + } catch (Exception e) {} + } + } + } + } + + public void stopProxy(int hostNumber, long millissecond) { + log.fine("stopping host "+hostNumber); + proxySet.get(currentType)[hostNumber - 1].restart(millissecond); + } + + public void stopProxy(int hostNumber) { + log.fine("stopping host "+hostNumber); + proxySet.get(currentType)[hostNumber - 1].stop(); + } + + public void restartProxy(int hostNumber) { + log.fine("restart host "+hostNumber); + if (hostNumber != -1) proxySet.get(currentType)[hostNumber - 1].restart(); + } + public void assureProxy() { + for (TcpProxy[] tcpProxies : proxySet.values()) { + for (TcpProxy tcpProxy : tcpProxies) { + tcpProxy.assureProxyOk(); + } + } + } + + public void assureBlackList(Connection connection) { + try { + Protocol protocol = getProtocolFromConnection(connection); + protocol.getProxy().getListener().getBlacklist().clear(); + } catch (Throwable e) { } + } + + + //does the user have super privileges or not? + public boolean hasSuperPrivilege(Connection connection, String testName) throws SQLException{ + boolean superPrivilege = false; + Statement st = connection.createStatement(); + + // first test for specific user and host combination + ResultSet rs = st.executeQuery("SELECT Super_Priv FROM mysql.user WHERE user = '" + username + "' AND host = '" + hostname + "'"); + if (rs.next()) { + superPrivilege = (rs.getString(1).equals("Y") ? true : false); + } else + { + // then check for user on whatever (%) host + rs = st.executeQuery("SELECT Super_Priv FROM mysql.user WHERE user = '" + username + "' AND host = '%'"); + if (rs.next()) + superPrivilege = (rs.getString(1).equals("Y") ? true : false); + } + + rs.close(); + + if (superPrivilege) + log.fine("test '" + testName + "' skipped because user '" + username + "' has SUPER privileges"); + + return superPrivilege; + } + + protected Protocol getProtocolFromConnection(Connection conn) throws Throwable { + + Method getProtocol = MySQLConnection.class.getDeclaredMethod("getProtocol", new Class[0]); + getProtocol.setAccessible(true); + return (Protocol) getProtocol.invoke(conn); + } + + public int getServerId(Connection connection) throws Throwable { + Protocol protocol = getProtocolFromConnection(connection); + HostAddress hostAddress = protocol.getHostAddress(); + List hostAddressList = protocol.getJdbcUrl().getHostAddresses(); + return hostAddressList.indexOf(hostAddress) + 1; + } + + public boolean inTransaction(Connection connection) throws Throwable { + Protocol protocol = getProtocolFromConnection(connection); + return protocol.inTransaction(); + } + boolean isMariadbServer(Connection connection) throws SQLException { + DatabaseMetaData md = connection.getMetaData(); + return md.getDatabaseProductVersion().indexOf("MariaDB") != -1; + } +} \ No newline at end of file diff --git a/src/test/java/org/mariadb/jdbc/failover/CancelTest.java b/src/test/java/org/mariadb/jdbc/failover/CancelTest.java new file mode 100644 index 000000000..d5ade0af8 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/CancelTest.java @@ -0,0 +1,60 @@ +package org.mariadb.jdbc.failover; + +import org.junit.*; +import org.mariadb.jdbc.BaseTest; + +import java.sql.*; +import java.util.Properties; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.junit.Assert.assertEquals; + +public class CancelTest extends BaseMultiHostTest { + private Connection connection; + + @Before + public void init() throws SQLException { + currentType = BaseMultiHostTest.TestType.GALERA; + initialUrl = initialGaleraUrl; + proxyUrl = proxyGaleraUrl; + Assume.assumeTrue(initialGaleraUrl != null); + connection = null; + } + + @After + public void after() throws SQLException { + assureProxy(); + assureBlackList(connection); + if (connection != null) connection.close(); + } + + + + @Test (expected = SQLTimeoutException.class) + public void timeoutSleep() throws Exception{ + connection = getNewConnection(false); + PreparedStatement stmt = connection.prepareStatement("select sleep(100)"); + stmt.setQueryTimeout(1); + stmt.execute(); + } + + @Test + public void NoTimeoutSleep() throws Exception{ + connection = getNewConnection(false); + Statement stmt = connection.createStatement(); + stmt.setQueryTimeout(1); + stmt.execute("select sleep(0.5)"); + + } + + @Test + public void CancelIdleStatement() throws Exception { + connection = getNewConnection(false); + Statement stmt = connection.createStatement(); + stmt.cancel(); + ResultSet rs = stmt.executeQuery("select 1"); + rs.next(); + assertEquals(rs.getInt(1),1); + } +} diff --git a/src/test/java/org/mariadb/jdbc/failover/GaleraFailoverTest.java b/src/test/java/org/mariadb/jdbc/failover/GaleraFailoverTest.java new file mode 100644 index 000000000..acd62adb0 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/GaleraFailoverTest.java @@ -0,0 +1,259 @@ +package org.mariadb.jdbc.failover; + +import org.junit.*; +import org.mariadb.jdbc.HostAddress; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.mysql.Protocol; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * test for galera + * The node must be configure with specific names : + * node 1 : wsrep_node_name = "galera1" + * ... + * node x : wsrep_node_name = "galerax" + * exemple mvn test -DdbUrl=jdbc:mysql://localhost:3306,localhost:3307/test?user=root + */ +public class GaleraFailoverTest extends BaseMultiHostTest { + private Connection connection; + + @Before + public void init() throws SQLException { + currentType = TestType.GALERA; + initialUrl = initialGaleraUrl; + proxyUrl = proxyGaleraUrl; + Assume.assumeTrue(initialGaleraUrl != null); + connection = null; + } + + @After + public void after() throws SQLException { + assureProxy(); + assureBlackList(connection); + if (connection != null) connection.close(); + } + + @Test + public void sequenceConnection() throws Throwable { + Assume.assumeTrue(!initialGaleraUrl.contains("failover")); + JDBCUrl jdbcUrl = JDBCUrl.parse(initialGaleraUrl); + for (int i = 0; i < jdbcUrl.getHostAddresses().size(); i++) { + connection = getNewConnection(true); + int serverNb = getServerId(connection); + Assert.assertTrue(serverNb == i + 1); + connection.close(); + stopProxy(serverNb); + } + } + + @Test + public void randomConnection() throws Throwable { + Assume.assumeTrue(initialGaleraUrl.contains("failover")); + Map connectionMap = new HashMap<>(); + for (int i = 0; i < 20; i++) { + connection = getNewConnection(false); + int serverId = getServerId(connection); + log.fine("master server found " + serverId); + MutableInt count = connectionMap.get(String.valueOf(serverId)); + if (count == null) { + connectionMap.put(String.valueOf(serverId), new MutableInt()); + } else { + count.increment(); + } + connection.close(); + } + + Assert.assertTrue(connectionMap.size() >= 2); + for (String key : connectionMap.keySet()) { + Integer connectionCount = connectionMap.get(key).get(); + log.fine(" ++++ Server " + key + " : " + connectionCount + " connections "); + Assert.assertTrue(connectionCount > 1); + } + log.fine("randomConnection OK"); + } + + @Test + public void checkStaticBlacklist() throws Throwable { + try { + connection = getNewConnection("&loadBalanceBlacklistTimeout=500", true); + Statement st = connection.createStatement(); + + int firstServerId = getServerId(connection); + stopProxy(firstServerId); + + try { + st.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + //normal exception that permit to blacklist the failing connection. + } + + //check blacklist size + try { + Protocol protocol = getProtocolFromConnection(connection); + log.fine("backlist size : " + protocol.getProxy().getListener().getBlacklist().size()); + Assert.assertTrue(protocol.getProxy().getListener().getBlacklist().size() == 1); + + //replace proxified HostAddress by normal one + JDBCUrl jdbcUrl = JDBCUrl.parse(initialUrl); + protocol.getProxy().getListener().getBlacklist().put(jdbcUrl.getHostAddresses().get(firstServerId - 1), System.currentTimeMillis()); + } catch (Throwable e) { + e.printStackTrace(); + Assert.fail(); + } + + //add first Host to blacklist + Protocol protocol = getProtocolFromConnection(connection); + protocol.getProxy().getListener().getBlacklist().size(); + + ExecutorService exec = Executors.newFixedThreadPool(2); + + //check blacklist shared + exec.execute(new CheckBlacklist(firstServerId, protocol.getProxy().getListener().getBlacklist())); + exec.execute(new CheckBlacklist(firstServerId, protocol.getProxy().getListener().getBlacklist())); + //wait for thread endings + + exec.shutdown(); + try { + exec.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + } + } catch (Throwable e) { + e.printStackTrace(); + Assert.fail(); + } + } + + protected class CheckBlacklist implements Runnable { + int firstServerId; + Map blacklist; + + public CheckBlacklist(int firstServerId, Map blacklist) { + this.firstServerId = firstServerId; + this.blacklist = blacklist; + } + + public void run() { + Connection connection2 = null; + try { + connection2 = getNewConnection(); + int otherServerId = getServerId(connection2); + log.fine("connected to server " + otherServerId); + Assert.assertTrue(otherServerId != firstServerId); + Protocol protocol = getProtocolFromConnection(connection2); + Assert.assertTrue(blacklist.keySet().toArray()[0].equals(protocol.getProxy().getListener().getBlacklist().keySet().toArray()[0])); + + } catch (Throwable e) { + e.printStackTrace(); + Assert.fail(); + } finally { + if (connection2 != null) { + try { + connection2.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + } + } + + @Test + public void testMultiHostWriteOnMaster() throws Throwable { + Assume.assumeTrue(initialGaleraUrl != null); + Connection connection = null; + log.fine("testMultiHostWriteOnMaster begin"); + try { + connection = getNewConnection(); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists multinode"); + stmt.execute("create table multinode (id int not null primary key auto_increment, test VARCHAR(10))"); + log.fine("testMultiHostWriteOnMaster OK"); + } finally { + assureProxy(); + assureBlackList(connection); + log.fine("testMultiHostWriteOnMaster done"); + if (connection != null) connection.close(); + } + } + + @Test + public void pingReconnectAfterRestart() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=1&failOnReadOnly=false&queriesBeforeRetryMaster=50000", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + long stoppedTime = System.currentTimeMillis(); + + try { + st.execute("SELECT 1"); + } catch (SQLException e) { + } + restartProxy(masterServerId); + long restartTime = System.currentTimeMillis(); + boolean loop = true; + while (loop) { + if (!connection.isClosed()) { + log.fine("reconnection with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + loop = false; + } + if (System.currentTimeMillis() - restartTime > 15 * 1000) Assert.fail(); + Thread.sleep(250); + } + } + + + + @Test + public void socketTimeoutTest() throws SQLException { + + // set a short connection timeout + connection = getNewConnection("&socketTimeout=15000&retriesAllDown=1", false); + + PreparedStatement ps = connection.prepareStatement("SELECT 1"); + ResultSet rs = ps.executeQuery(); + rs.next(); + + // wait for the connection to time out + ps = connection.prepareStatement("SELECT sleep(16)"); + + // a timeout should occur here + try { + ps.executeQuery(); + Assert.fail(); + } catch (SQLException e) { + Assert.assertTrue(e.getMessage().contains("timed out")); + } + try { + ps = connection.prepareStatement("SELECT 2"); + ps.execute(); + } catch (Exception e){ + Assert.fail(); + } + + // the connection should not be closed + assertTrue(!connection.isClosed()); + } + + class MutableInt { + int value = 1; // note that we start at 1 since we're counting + + public void increment() { + ++value; + } + + public int get() { + return value; + } + } + +} diff --git a/src/test/java/org/mariadb/jdbc/failover/MonoServerFailoverTest.java b/src/test/java/org/mariadb/jdbc/failover/MonoServerFailoverTest.java new file mode 100644 index 000000000..9854911e2 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/MonoServerFailoverTest.java @@ -0,0 +1,190 @@ +package org.mariadb.jdbc.failover; + +import org.junit.*; +import org.mariadb.jdbc.JDBCUrl; +import org.mariadb.jdbc.internal.mysql.Protocol; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertFalse; + +public class MonoServerFailoverTest extends BaseMultiHostTest { + private Connection connection; + + @Before + public void init() throws SQLException { + Assume.assumeTrue(initialUrl != null); + currentType = TestType.NONE; + } + + @After + public void after() throws SQLException { + assureProxy(); + assureBlackList(connection); + if (connection != null) connection.close(); + } + + @Test + public void checkClosedConnectionAfterFailover() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + st.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + } + Assert.assertTrue(st.isClosed()); + restartProxy(masterServerId); + try { + st = connection.createStatement(); + st.execute("SELECT 1"); + } catch (SQLException e) { + Assert.fail(); + } + + } + + @Test + public void checkErrorAfterDeconnection() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + st.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + } + + restartProxy(masterServerId); + try { + st.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + //statement must be closed -> error + } + Assert.assertTrue(connection.isClosed()); + + } + + + @Test + public void checkAutoReconnectDeconnection() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + st.execute("SELECT 1"); + Assert.fail(); + } catch (SQLException e) { + } + + restartProxy(masterServerId); + try { + //with autoreconnect -> not closed + st = connection.createStatement(); + st.execute("SELECT 1"); + } catch (SQLException e) { + Assert.fail(); + } + Assert.assertFalse(connection.isClosed()); + + + } + + + /** + * CONJ-120 Fix Connection.isValid method + * + * @throws Exception + */ + @Test + public void isValid_connectionThatIsKilledExternally() throws Throwable { + Connection killerConnection = null; + try { + connection = getNewConnection(); + connection.setCatalog("mysql"); + Protocol protocol = getProtocolFromConnection(connection); + killerConnection = getNewConnection(); + Statement killerStatement = killerConnection.createStatement(); + long threadId = protocol.getServerThreadId(); + killerStatement.execute("KILL CONNECTION " + threadId); + killerConnection.close(); + boolean isValid = connection.isValid(0); + assertFalse(isValid); + } finally { + killerConnection.close(); + } + } + + @Test + public void checkPrepareStatement() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists failt1"); + stmt.execute("create table failt1 (id int not null primary key auto_increment, tt int)"); + + + PreparedStatement preparedStatement = connection.prepareStatement("insert into failt1(id, tt) values (?,?)"); + + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + preparedStatement.setInt(1, 1); + preparedStatement.setInt(2, 1); + preparedStatement.addBatch(); + try { + preparedStatement.executeBatch(); + Assert.fail(); + } catch (SQLException e) { + + } + restartProxy(masterServerId); + try { + preparedStatement.executeBatch(); + } catch (SQLException e) { + Assert.fail(); + } + } + +/* + @Test + public void failoverDuringStreamStatement() throws Throwable { + connection = getNewConnection("&autoReconnect=true", true); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists failt2"); + stmt.execute("create table failt2 (tt int)"); + + PreparedStatement preparedStatement = connection.prepareStatement("insert into failt2(tt) values (?)"); + for (int i=0; i<100;i++) { + preparedStatement.setInt(1, i); + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + stmt = connection.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); + stmt.setFetchSize(Integer.MIN_VALUE); + ResultSet rs = stmt.executeQuery("SELECT * FROM failt2"); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + int nbRead = 0; + try { + while (rs.next()) { + nbRead++; + log.fine("nbRead = "+nbRead + " rs="+rs.getInt(1)); + } + Assert.fail(); + } catch (SQLException e) { + Assert.assertTrue(nbRead == 10); + } + } +*/ +} diff --git a/src/test/java/org/mariadb/jdbc/failover/OldFailoverTest.java b/src/test/java/org/mariadb/jdbc/failover/OldFailoverTest.java new file mode 100644 index 000000000..2b1b03d6a --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/OldFailoverTest.java @@ -0,0 +1,49 @@ +package org.mariadb.jdbc.failover; + +import org.junit.Assert; +import org.junit.Test; +import org.mariadb.jdbc.BaseTest; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertFalse; + +public class OldFailoverTest extends BaseTest { + + /** + * check old connection way before multihost was handle + * @throws Exception + */ + @Test + public void isOldConfigurationValid() throws Exception { + String falseUrl = "jdbc:mysql://localhost:1111," + hostname + ":" + port + "/" + database+"?user=" + username + + (password != null && !"".equals(password) ? "&password=" + password : "") + + (parameters != null ? parameters : ""); + + try { + //the first host doesn't exist, so with the random host selection, verifying that we connect to the good host + for (int i=0;i<10;i++) { + Connection tmpConnection = openNewConnection(falseUrl); + Statement tmpStatement = tmpConnection.createStatement(); + tmpStatement.execute("SELECT 1"); + } + } catch (Exception e) { + Assert.fail(); + } + } + + @Test + public void errorUrl() throws Exception { + String falseUrl = "jdbc:mysql://localhost:1111/test"; + + try { + Connection tmpConnection = openNewConnection(falseUrl); + Assert.fail(); + } catch (Exception e) { + } + } + + +} diff --git a/src/test/java/org/mariadb/jdbc/failover/ReplicationFailoverTest.java b/src/test/java/org/mariadb/jdbc/failover/ReplicationFailoverTest.java new file mode 100644 index 000000000..e5a2bcd22 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/ReplicationFailoverTest.java @@ -0,0 +1,458 @@ +package org.mariadb.jdbc.failover; + +import org.junit.After; +import static org.junit.Assert.*; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.HashMap; +import java.util.Map; + +public class ReplicationFailoverTest extends BaseMultiHostTest { + private Connection connection; + private long testBeginTime; + + @Before + public void init() throws SQLException { + initialUrl = initialReplicationUrl; + proxyUrl = proxyReplicationUrl; + Assume.assumeTrue(initialReplicationUrl != null); + connection = null; + currentType = TestType.REPLICATION; + testBeginTime=System.currentTimeMillis(); + } + + @After + public void after() throws SQLException { + assureProxy(); + assureBlackList(connection); + if (connection != null) connection.close(); + + log.fine("test time : "+(System.currentTimeMillis() - testBeginTime) + "ms"); + } + + @Test + public void testWriteOnMaster() throws SQLException { + connection = getNewConnection(false); + Statement stmt = connection.createStatement(); + stmt.execute("drop table if exists multinode"); + stmt.execute("create table multinode (id int not null primary key auto_increment, test VARCHAR(10))"); + } + + @Test + public void testErrorWriteOnSlave() throws SQLException { + connection = getNewConnection(false); + connection.setReadOnly(true); + Statement stmt = connection.createStatement(); + assertTrue(connection.isReadOnly()); + try { + if (!isMariadbServer(connection) || !requireMinimumVersion(connection, 10, 0)) { + //on version > 10 use SESSION READ-ONLY, before no control + Assume.assumeTrue(false); + } + stmt.execute("drop table if exists multinode4"); + log.severe("ERROR - > must not be able to write on slave "); + fail(); + } catch (SQLException e) { + } + } + + @Test + public void randomConnection() throws Throwable { + Map connectionMap = new HashMap(); + int masterId = -1; + for (int i = 0; i < 20; i++) { + connection = getNewConnection("&retriesAllDown=1", false); + int serverId = getServerId(connection); + log.fine("master server found " + serverId); + if (i > 0) assertTrue(masterId == serverId); + masterId = serverId; + connection.setReadOnly(true); + int replicaId = getServerId(connection); + log.fine("++++++++++++slave server found " + replicaId); + MutableInt count = connectionMap.get(String.valueOf(replicaId)); + if (count == null) { + connectionMap.put(String.valueOf(replicaId), new MutableInt()); + } else { + count.increment(); + } + connection.close(); + } + + assertTrue(connectionMap.size() >= 2); + for (String key : connectionMap.keySet()) { + Integer connectionCount = connectionMap.get(key).get(); + log.fine(" ++++ Server " + key + " : " + connectionCount + " connections "); + assertTrue(connectionCount > 1); + } + } + + @Test + public void failoverSlaveToMaster() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + assertFalse(masterServerId == slaveServerId); + stopProxy(slaveServerId); + connection.createStatement().execute("SELECT 1"); + int currentServerId = getServerId(connection); + + log.fine("masterServerId = " + masterServerId + "/currentServerId = " + currentServerId); + assertTrue(masterServerId == currentServerId); + + assertFalse(connection.isReadOnly()); + } + @Test + public void failoverSlaveToMasterFail() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&failOnReadOnly=true", true); + int masterServerId = getServerId(connection); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + assertTrue(slaveServerId != masterServerId); + connection.setReadOnly(false); + stopProxy(masterServerId); + + long failTime = System.currentTimeMillis(); + connection.createStatement().execute("SELECT 1"); + assertTrue(System.currentTimeMillis() - failTime < 100); + assertTrue(slaveServerId == getServerId(connection) ); + } + + @Test + public void pingReconnectAfterFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=5&failOnReadOnly=false&queriesBeforeRetryMaster=50000", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + try { + st.execute("SELECT 1"); + } catch (SQLException e) {} + + connection.setReadOnly(true); + st = connection.createStatement(); + restartProxy(masterServerId); + try { + connection.setReadOnly(false); + fail(); + } catch (SQLException e) {} + + long stoppedTime = System.currentTimeMillis(); + + boolean loop = true; + while (loop) { + try { + Thread.sleep(250); + log.fine("time : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + int currentHost = getServerId(connection); + if (masterServerId == currentHost) { + log.fine("reconnection with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + assertTrue((System.currentTimeMillis() - stoppedTime) > 5 * 1000); + loop = false; + } + } catch (SQLException e) { + } + if (System.currentTimeMillis() - stoppedTime > 20 * 1000) fail(); + } + } + + @Test + public void failoverDuringMasterSetReadOnly() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + assertFalse(slaveServerId == masterServerId); + assertTrue(connection.isReadOnly()); + } + + @Test + public void failoverDuringSlaveSetReadOnly() throws Throwable { + connection = getNewConnection(true); + connection.setReadOnly(true); + int slaveServerId = getServerId(connection); + stopProxy(slaveServerId, 2000); + connection.setReadOnly(false); + int masterServerId = getServerId(connection); + assertFalse(slaveServerId == masterServerId); + assertFalse(connection.isReadOnly()); + } + @Test() + public void changeSlave() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + connection.setReadOnly(true); + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + try { + connection.createStatement().executeQuery("SELECT CONNECTION_ID()"); + } catch (SQLException e) { + fail(); + } + } + + @Test() + public void masterWithoutFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + connection.setReadOnly(true); + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + connection.setReadOnly(false); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + try { + connection.createStatement().executeQuery("SELECT CONNECTION_ID()"); + fail(); + } catch (SQLException e) { + assertTrue(true); + } + } + + @Test + public void failoverSlaveAndMasterWithAutoConnect() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + + //search actual server_id for master and slave + int masterServerId = getServerId(connection); + log.fine("master server_id = " + masterServerId); + + connection.setReadOnly(true); + + int firstSlaveId = getServerId(connection); + log.fine("slave1 server_id = " + firstSlaveId); + + stopProxy(masterServerId); + stopProxy(firstSlaveId); + + //must reconnect to the second slave without error + connection.createStatement().execute("SELECT 1"); + int currentSlaveId = getServerId(connection); + log.fine("currentSlaveId server_id = " + currentSlaveId); + assertTrue(currentSlaveId != firstSlaveId); + assertTrue(currentSlaveId != masterServerId); + } + + @Test + public void failoverMasterWithAutoConnect() throws Throwable { + connection = getNewConnection("&autoReconnect=true&retriesAllDown=1", true); + int masterServerId = getServerId(connection); + + stopProxy(masterServerId, 250); + //with autoreconnect, the connection must reconnect automatically + int currentServerId = getServerId(connection); + + assertTrue(currentServerId == masterServerId); + assertFalse(connection.isReadOnly()); + } + + @Test + public void checkReconnectionToMasterAfterQueryNumber() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=3000&queriesBeforeRetryMaster=10&failOnReadOnly=true", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + st.execute("SELECT 1"); + } catch (SQLException e) { + fail(); + } + assertTrue(connection.isReadOnly()); + + restartProxy(masterServerId); + + //not in autoreconnect mode, so must wait for query more than queriesBeforeRetryMaster + for (int i = 1; i < 10; i++) { + try { + st.execute("SELECT 1"); + log.fine("i=" + i); + assertTrue(connection.isReadOnly()); + } catch (SQLException e) { + fail(); + } + } + Thread.sleep(5000); + long startTime = System.currentTimeMillis(); + connection.setReadOnly(false); + log.fine(" time = " + (System.currentTimeMillis() - startTime)); + assertTrue(System.currentTimeMillis() - startTime < 4000); + + } + + @Test + public void reconnectMasterAfterFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1", true); + //if super user can write on slave + Assume.assumeTrue(!hasSuperPrivilege(connection, "reconnectMasterAfterFailover")); + Statement st = connection.createStatement(); + st.execute("drop table if exists multinode2"); + st.execute("create table multinode2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + st.execute("insert into multinode2 (id, amount) VALUE (1 , 100)"); + + int masterServerId = getServerId(connection); + long stopTime = System.currentTimeMillis(); + stopProxy(masterServerId, 10000); + try { + st.execute("insert into multinode2 (id, amount) VALUE (2 , 100)"); + assertTrue(System.currentTimeMillis() - stopTime > 10); + assertTrue(System.currentTimeMillis() - stopTime < 20); + } catch (SQLException e) { + } + } + + @Test + public void writeToSlaveAfterFailover() throws Throwable { + connection = getNewConnection("&retriesAllDown=1",true); + //if super user can write on slave + Assume.assumeTrue(!hasSuperPrivilege(connection, "writeToSlaveAfterFailover")); + Statement st = connection.createStatement(); + st.execute("drop table if exists multinode2"); + st.execute("create table multinode2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + st.execute("insert into multinode2 (id, amount) VALUE (1 , 100)"); + + int masterServerId = getServerId(connection); + + stopProxy(masterServerId); + try { + st.execute("insert into multinode2 (id, amount) VALUE (2 , 100)"); + fail(); + } catch (SQLException e) { + } + } + + + @Test + public void checkBackOnMasterOnSlaveFail() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&secondsBeforeRetryMaster=10&failOnReadOnly=true", true); + Statement st = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + + try { + st.execute("SELECT 1"); + assertTrue(connection.isReadOnly()); + } catch (SQLException e) { + fail(); + } + + long stoppedTime = System.currentTimeMillis(); + restartProxy(masterServerId); + boolean loop = true; + while (loop) { + Thread.sleep(250); + try { + if (!connection.isReadOnly()) { + log.fine("reconnection to master with failover loop after : " + (System.currentTimeMillis() - stoppedTime) + "ms"); + assertTrue((System.currentTimeMillis() - stoppedTime) > 10 * 1000); + loop = false; + } + } catch (SQLException e) { + } + if (System.currentTimeMillis() - stoppedTime > 30 * 1000) fail(); + } + } + + + @Test() + public void checkNoSwitchConnectionDuringTransaction() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true", false); + Statement st = connection.createStatement(); + + st.execute("drop table if exists multinodeTransaction2"); + st.execute("create table multinodeTransaction2 (id int not null primary key , amount int not null) ENGINE = InnoDB"); + connection.setAutoCommit(false); + st.execute("insert into multinodeTransaction2 (id, amount) VALUE (1 , 100)"); + + try { + //in transaction, so must trow an error + connection.setReadOnly(true); + fail(); + } catch (SQLException e) { + } + } + + @Test + public void failoverMasterWithAutoConnectAndTransaction() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnect=true", true); + Statement st = connection.createStatement(); + + int masterServerId = getServerId(connection); + st.execute("drop table if exists multinodeTransaction"); + st.execute("create table multinodeTransaction (id int not null primary key , amount int not null) ENGINE = InnoDB"); + connection.setAutoCommit(false); + st.execute("insert into multinodeTransaction (id, amount) VALUE (1 , 100)"); + stopProxy(masterServerId); + assertTrue(inTransaction(connection)); + try { + //with autoreconnect but in transaction, query must throw an error + st.execute("insert into multinodeTransaction (id, amount) VALUE (2 , 10)"); + fail(); + } catch (SQLException e) { + } + restartProxy(masterServerId); + try { + st = connection.createStatement(); + // will try a ping, if ok, if not, transaction is considered be lost + st.execute("insert into multinodeTransaction (id, amount) VALUE (2 , 10)"); + } catch (SQLException e) { + fail(); + } + } + + @Test + public void testFailNotOnSlave() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&autoReconnectMaster=true&failOnReadOnly=false", true); + Statement stmt = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + stmt.execute("SELECT 1"); + fail(); + } catch (SQLException e) { + //normal error + } + assertTrue(!connection.isReadOnly()); + } + + @Test + public void testAutoReconnectMasterFailSlave() throws Throwable { + connection = getNewConnection("&retriesAllDown=1&failOnReadOnly=true", true); + Statement stmt = connection.createStatement(); + int masterServerId = getServerId(connection); + stopProxy(masterServerId); + try { + stmt.execute("SELECT 1"); + } catch (SQLException e) { + fail(); + } + assertTrue(connection.isReadOnly()); + } + + class MutableInt { + int value = 1; // note that we start at 1 since we're counting + + public void increment() { + ++value; + } + + public int get() { + return value; + } + } + +} diff --git a/src/test/java/org/mariadb/jdbc/failover/TcpProxy.java b/src/test/java/org/mariadb/jdbc/failover/TcpProxy.java new file mode 100644 index 000000000..64ea86b75 --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/TcpProxy.java @@ -0,0 +1,51 @@ +package org.mariadb.jdbc.failover; + +import java.io.*; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.Executors; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class TcpProxy { + protected static Logger log = Logger.getLogger("org.maria.jdbc"); + + String host; + int remoteport; + TcpProxySocket socket; + + public TcpProxy(String host, int remoteport) throws IOException { + this.host = host; + this.remoteport = remoteport; + socket = new TcpProxySocket(host, remoteport); + Executors.newSingleThreadScheduledExecutor().schedule(socket, 0, TimeUnit.MILLISECONDS); + } + + public void restart(long sleepTime) { + socket.kill(); + Executors.newSingleThreadScheduledExecutor().schedule(socket, sleepTime, TimeUnit.MILLISECONDS); + } + + public void stop() { + socket.kill(); + } + + public void restart() { + Executors.newSingleThreadExecutor().execute(socket); + try { + Thread.sleep(10); + }catch(InterruptedException e) {} + } + public void assureProxyOk() { + if (socket.isClosed()) { + restart(); + } + } + + public int getLocalPort() { + return socket.getLocalPort(); + } + +} diff --git a/src/test/java/org/mariadb/jdbc/failover/TcpProxySocket.java b/src/test/java/org/mariadb/jdbc/failover/TcpProxySocket.java new file mode 100644 index 000000000..b68f8619a --- /dev/null +++ b/src/test/java/org/mariadb/jdbc/failover/TcpProxySocket.java @@ -0,0 +1,129 @@ +package org.mariadb.jdbc.failover; + +import java.io.*; +import java.net.BindException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.Executors; +import java.util.concurrent.RunnableScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +public class TcpProxySocket implements Runnable { + protected static Logger log = Logger.getLogger("org.maria.jdbc"); + + String host; + int remoteport; + int localport; + boolean stop = false; + Socket client = null, server = null; + ServerSocket ss; + + public TcpProxySocket(String host, int remoteport) throws IOException { + this.host = host; + this.remoteport = remoteport; + ss = new ServerSocket(0); + this.localport = ss.getLocalPort(); + } + + public int getLocalPort() { + return ss.getLocalPort(); + } + + public boolean isClosed() { + return ss.isClosed(); + } + + public void kill() { + stop = true; + try { + if (server != null) server.close(); + } catch (IOException e) { } + try { + if (client != null) client.close(); + } catch (IOException e) { } + try { + ss.close(); + } catch (IOException e) { } + } + + @Override + public void run() { + + stop = false; + try { + try { + if (ss.isClosed()) ss = new ServerSocket(localport); + } catch (BindException b) { + //in case for testing crash and reopen too quickly + try { + Thread.sleep(100); + } catch (InterruptedException i) { } + if (ss.isClosed()) ss = new ServerSocket(localport); + } + final byte[] request = new byte[1024]; + byte[] reply = new byte[4096]; + while (!stop) { + try { + client = ss.accept(); + final InputStream from_client = client.getInputStream(); + final OutputStream to_client = client.getOutputStream(); + try { + server = new Socket(host, remoteport); + } catch (IOException e) { + PrintWriter out = new PrintWriter(new OutputStreamWriter(to_client)); + out.println("Proxy server cannot connect to " + host + ":" + + remoteport + ":\n" + e); + out.flush(); + client.close(); + continue; + } + final InputStream from_server = server.getInputStream(); + final OutputStream to_server = server.getOutputStream(); + new Thread() { + public void run() { + int bytes_read; + try { + while ((bytes_read = from_client.read(request)) != -1) { + to_server.write(request, 0, bytes_read); + log.finest(bytes_read + "to_server--->" + new String(request, "UTF-8") + "<---"); + to_server.flush(); + } + } catch (IOException e) { + } + try { + to_server.close(); + } catch (IOException e) { } + } + }.start(); + int bytes_read; + try { + while ((bytes_read = from_server.read(reply)) != -1) { + try { + Thread.sleep(1); + log.finest(bytes_read + " to_client--->" + new String(reply, "UTF-8") + "<---"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + to_client.write(reply, 0, bytes_read); + to_client.flush(); + } + } catch (IOException e) { + } + to_client.close(); + } catch (IOException e) { + //System.err.println("ERROR socket : "+e); + } + finally { + try { + if (server != null) server.close(); + if (client != null) client.close(); + } catch (IOException e) { + } + } + } + } catch ( IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/test/resources/logging.properties b/src/test/resources/logging.properties new file mode 100644 index 000000000..f02449094 --- /dev/null +++ b/src/test/resources/logging.properties @@ -0,0 +1,6 @@ +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.level = FINE +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +# Pattern works since Java 7 +java.util.logging.SimpleFormatter.format = [%1$tc] %4$s: %2$s - %5$s %6$s%n +org.mariadb.jdbc.level=FINE \ No newline at end of file