Skip to content

Commit

Permalink
fix: query timeout kill query directed to incorrect instance
Browse files Browse the repository at this point in the history
  • Loading branch information
crystall-bitquill committed Oct 13, 2023
1 parent ec12d1b commit bc6f795
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,38 @@ public static HostInfo createHostWithProperties(HostInfo baseHost, Map<String, S
propertiesCopy);
}

/**
* Create a copy of {@link HostInfo} object where all properties are the same but the hostInfo points to the provided
* instance.
*
* @param instance The instance name to replace the previous host.
* @param previousHostInfo The {@link HostInfo} object that all other information to add to the new {@link HostInfo}.
*
* @return A copy of {@link HostInfo} object where only the host has been changed.
*/
public static HostInfo createInstanceHostWithProperties(String instance, HostInfo previousHostInfo)
throws SQLException {
final Properties propertiesCopy = new Properties();
propertiesCopy.putAll(previousHostInfo.getHostProperties());
propertiesCopy.put(PropertyKey.USER.getKeyName(), previousHostInfo.getUser());
propertiesCopy.put(PropertyKey.PASSWORD.getKeyName(), previousHostInfo.getPassword());

final ConnectionUrl hostUrl = ConnectionUrl.getConnectionUrlInstance(
getUrlFromEndpoint(
instance,
previousHostInfo.getPort(),
propertiesCopy),
propertiesCopy);

return new HostInfo(
hostUrl,
instance,
previousHostInfo.getPort(),
previousHostInfo.getUser(),
previousHostInfo.getPassword(),
previousHostInfo.getHostProperties());
}

public static String getUrlFromEndpoint(String endpoint, int port, Properties props) throws SQLException {
final Properties propsCopy = new Properties();
propsCopy.putAll(props);
Expand All @@ -172,8 +204,10 @@ public static String getUrlFromEndpoint(String endpoint, int port, Properties pr
);

final String dbName = propsCopy.getProperty(PropertyKey.DBNAME.getKeyName());
boolean containsDbName = false;
if (!StringUtils.isNullOrEmpty(dbName)) {
urlBuilder.append("/").append(dbName);
containsDbName = true;
}
propsCopy.remove(PropertyKey.DBNAME.getKeyName());

Expand Down Expand Up @@ -202,7 +236,7 @@ public static String getUrlFromEndpoint(String endpoint, int port, Properties pr

if (queryBuilder.length() != 0) {
urlBuilder.append("?").append(queryBuilder);
} else {
} else if (!containsDbName) {
urlBuilder.append("/");
}

Expand Down
142 changes: 142 additions & 0 deletions src/main/user-impl/java/com/mysql/cj/jdbc/ha/RdsUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2.0
* (GPLv2), as published by the Free Software Foundation, with the
* following additional permissions:
*
* This program is distributed with certain software that is licensed
* under separate terms, as designated in a particular file or component
* or in the license documentation. Without limiting your rights under
* the GPLv2, the authors of this program hereby grant you an additional
* permission to link the program and your derivative works with the
* separately licensed software that they have included with the program.
*
* Without limiting the foregoing grant of rights under the GPLv2 and
* additional permission as to separately licensed software, this
* program is also subject to the Universal FOSS Exception, version 1.0,
* a copy of which can be found along with its FAQ at
* http://oss.oracle.com/licenses/universal-foss-exception.
*
* This program 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 General Public License, version 2.0, for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see
* http://www.gnu.org/licenses/gpl-2.0.html.
*/

package com.mysql.cj.jdbc.ha;

import com.mysql.cj.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RdsUtils {

// Aurora DB clusters support different endpoints. More details about Aurora RDS endpoints
// can be found at
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.Endpoints.html
//
// Details how to use RDS Proxy endpoints can be found at
// https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/rds-proxy-endpoints.html
//
// Values like "<...>" depend on particular Aurora cluster.
// For example: "<database-cluster-name>"
//
//
//
// Cluster (Writer) Endpoint: <database-cluster-name>.cluster-<xyz>.<aws-region>.rds.amazonaws.com
// Example: test-postgres.cluster-123456789012.us-east-2.rds.amazonaws.com
//
// Cluster Reader Endpoint: <database-cluster-name>.cluster-ro-<xyz>.<aws-region>.rds.amazonaws.com
// Example: test-postgres.cluster-ro-123456789012.us-east-2.rds.amazonaws.com
//
// Cluster Custom Endpoint: <cluster-name-alias>.cluster-custom-<xyz>.<aws-region>.rds.amazonaws.com
// Example: test-postgres-alias.cluster-custom-123456789012.us-east-2.rds.amazonaws.com
//
// Instance Endpoint: <instance-name>.<xyz>.<aws-region>.rds.amazonaws.com
// Example: test-postgres-instance-1.123456789012.us-east-2.rds.amazonaws.com
//
//
//
// Similar endpoints for China regions have different structure and are presented below.
//
// Cluster (Writer) Endpoint: <database-cluster-name>.cluster-<xyz>.rds.<aws-region>.amazonaws.com.cn
// Example: test-postgres.cluster-123456789012.rds.cn-northwest-1.amazonaws.com.cn
//
// Cluster Reader Endpoint: <database-cluster-name>.cluster-ro-<xyz>.rds.<aws-region>.amazonaws.com.cn
// Example: test-postgres.cluster-ro-123456789012.rds.cn-northwest-1.amazonaws.com.cn
//
// Cluster Custom Endpoint: <cluster-name-alias>.cluster-custom-<xyz>.rds.<aws-region>.amazonaws.com.cn
// Example: test-postgres-alias.cluster-custom-123456789012.rds.cn-northwest-1.amazonaws.com.cn
//
// Instance Endpoint: <instance-name>.<xyz>.rds.<aws-region>.amazonaws.com.cn
// Example: test-postgres-instance-1.123456789012.rds.cn-northwest-1.amazonaws.com.cn

private static final Pattern AURORA_DNS_PATTERN =
Pattern.compile(
"(?<instance>.+)\\."
+ "(?<dns>proxy-|cluster-|cluster-ro-|cluster-custom-)?"
+ "(?<domain>[a-zA-Z0-9]+\\.(?<region>[a-zA-Z0-9\\-]+)\\.rds\\.amazonaws\\.com)",
Pattern.CASE_INSENSITIVE);

private static final Pattern AURORA_CLUSTER_PATTERN =
Pattern.compile(
"(?<instance>.+)\\."
+ "(?<dns>cluster-|cluster-ro-)+"
+ "(?<domain>[a-zA-Z0-9]+\\.(?<region>[a-zA-Z0-9\\-]+)\\.rds\\.amazonaws\\.com)",
Pattern.CASE_INSENSITIVE);

private static final Pattern AURORA_CHINA_DNS_PATTERN =
Pattern.compile(
"(?<instance>.+)\\."
+ "(?<dns>proxy-|cluster-|cluster-ro-|cluster-custom-)?"
+ "(?<domain>[a-zA-Z0-9]+\\.rds\\.(?<region>[a-zA-Z0-9\\-]+)\\.amazonaws\\.com\\.cn)",
Pattern.CASE_INSENSITIVE);

private static final Pattern AURORA_CHINA_CLUSTER_PATTERN =
Pattern.compile(
"(?<instance>.+)\\."
+ "(?<dns>cluster-|cluster-ro-)+"
+ "(?<domain>[a-zA-Z0-9]+\\.rds\\.(?<region>[a-zA-Z0-9\\-]+)\\.amazonaws\\.com\\.cn)",
Pattern.CASE_INSENSITIVE);

private static final String DNS_GROUP = "dns";
private static final String DOMAIN_GROUP = "domain";

public String getRdsInstanceHostPattern(final String host) {
if (StringUtils.isNullOrEmpty(host)) {
return "?";
}

final Matcher matcher = AURORA_DNS_PATTERN.matcher(host);
if (matcher.find()) {
return "?." + matcher.group(DOMAIN_GROUP);
}
final Matcher chinaMatcher = AURORA_CHINA_DNS_PATTERN.matcher(host);
if (chinaMatcher.find()) {
return "?." + chinaMatcher.group(DOMAIN_GROUP);
}
return "?";
}

public boolean isReaderClusterDns(final String host) {
if (StringUtils.isNullOrEmpty(host)) {
return false;
}

final Matcher matcher = AURORA_CLUSTER_PATTERN.matcher(host);
if (matcher.find()) {
return "cluster-ro-".equalsIgnoreCase(matcher.group(DNS_GROUP));
}
final Matcher chinaMatcher = AURORA_CHINA_CLUSTER_PATTERN.matcher(host);
if (chinaMatcher.find()) {
return "cluster-ro-".equalsIgnoreCase(chinaMatcher.group(DNS_GROUP));
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,17 @@

import com.mysql.cj.conf.ConnectionUrl;
import com.mysql.cj.conf.HostInfo;
import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.jdbc.JdbcConnection;
import com.mysql.cj.jdbc.ha.ConnectionUtils;
import com.mysql.cj.jdbc.ha.RdsUtils;
import com.mysql.cj.log.Log;

import com.mysql.cj.util.StringUtils;
import java.lang.reflect.InvocationTargetException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.Callable;

/**
Expand All @@ -49,6 +55,7 @@ public class DefaultConnectionPlugin implements IConnectionPlugin {
protected Log logger;
protected IConnectionProvider connectionProvider;
protected final ICurrentConnectionProvider currentConnectionProvider;
private static String GET_INSTANCE_QUERY = "SELECT @@aurora_server_id";

public DefaultConnectionPlugin(ICurrentConnectionProvider currentConnectionProvider, Log logger) {
this(currentConnectionProvider, logger, new BasicConnectionProvider());
Expand Down Expand Up @@ -102,6 +109,22 @@ public void openInitialConnection(ConnectionUrl connectionUrl) throws SQLExcepti

HostInfo mainHostInfo = connectionUrl.getMainHost();
JdbcConnection connection = this.connectionProvider.connect(mainHostInfo);

RdsUtils rdsUtils = new RdsUtils();
if (rdsUtils.isReaderClusterDns(mainHostInfo.getHost())) {
final String connectedHostName = getCurrentlyConnectedInstance(connection);
String instanceEndpoint =
rdsUtils.getRdsInstanceHostPattern(mainHostInfo.getHost()).replace("?", connectedHostName);
if (!StringUtils.isNullOrEmpty(mainHostInfo.getHostProperties().get(PropertyKey.clusterInstanceHostPattern.getKeyName()))) {
instanceEndpoint = mainHostInfo.getHostProperties().get(PropertyKey.clusterInstanceHostPattern.getKeyName())
.replace("?", connectedHostName);
}
final HostInfo instanceHostInfo = ConnectionUtils.createInstanceHostWithProperties(instanceEndpoint, mainHostInfo);
JdbcConnection hostConnection = this.connectionProvider.connect(instanceHostInfo);
connection.close();
connection = hostConnection;
}

this.currentConnectionProvider.setCurrentConnection(connection, mainHostInfo);
}

Expand All @@ -119,4 +142,12 @@ public void transactionCompleted() {
public void releaseResources() {
// do nothing
}

private String getCurrentlyConnectedInstance(JdbcConnection connection) throws SQLException {
try (final Statement statement = connection.createStatement()) {
final ResultSet rs = statement.executeQuery(GET_INSTANCE_QUERY);
rs.next();
return rs.getString(1);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
package testsuite.integration.container;

import com.mysql.cj.conf.PropertyKey;
import com.mysql.cj.jdbc.exceptions.MySQLTimeoutException;
import com.mysql.cj.jdbc.ha.plugins.failover.IClusterAwareMetricsReporter;
import java.sql.Statement;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -71,20 +73,29 @@ public class AuroraMysqlIntegrationTest extends AuroraMysqlIntegrationBaseTest {

@ParameterizedTest(name = "test_ConnectionString")
@MethodSource("generateConnectionString")
public void test_ConnectionString(String connStr, int port) throws SQLException {
final Connection conn = connectToInstance(connStr, port);
public void test_ConnectionString(String connStr, int port, Properties props) throws SQLException {
final Connection conn = connectToInstance(connStr, port, props);
assertTrue(conn.isValid(5));
conn.close();
}

private static Stream<Arguments> generateConnectionString() {
final Properties props = new Properties();
props.setProperty(PropertyKey.USER.getKeyName(), TEST_USERNAME);
props.setProperty(PropertyKey.PASSWORD.getKeyName(), TEST_PASSWORD);
props.setProperty(PropertyKey.tcpKeepAlive.getKeyName(), Boolean.FALSE.toString());
props.setProperty(PropertyKey.connectTimeout.getKeyName(), "3000");
props.setProperty(PropertyKey.socketTimeout.getKeyName(), "3000");
final Properties proxyProps = new Properties();
proxyProps.putAll(props);
proxyProps.setProperty(PropertyKey.clusterInstanceHostPattern.getKeyName(), PROXIED_CLUSTER_TEMPLATE);
return Stream.of(
Arguments.of(MYSQL_INSTANCE_1_URL, MYSQL_PORT),
Arguments.of(MYSQL_INSTANCE_1_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT),
Arguments.of(MYSQL_CLUSTER_URL, MYSQL_PORT),
Arguments.of(MYSQL_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT),
Arguments.of(MYSQL_RO_CLUSTER_URL, MYSQL_PORT),
Arguments.of(MYSQL_RO_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT)
Arguments.of(MYSQL_INSTANCE_1_URL, MYSQL_PORT, props),
Arguments.of(MYSQL_INSTANCE_1_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT, proxyProps),
Arguments.of(MYSQL_CLUSTER_URL, MYSQL_PORT, props),
Arguments.of(MYSQL_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT, proxyProps),
Arguments.of(MYSQL_RO_CLUSTER_URL, MYSQL_PORT, props),
Arguments.of(MYSQL_RO_CLUSTER_URL + PROXIED_DOMAIN_NAME_SUFFIX, MYSQL_PROXY_PORT, proxyProps)
);
}

Expand Down Expand Up @@ -607,4 +618,16 @@ public void test_PreparedStatementHashCodes() throws SQLException, IOException {
conn.close();
}

@RepeatedTest(100)
public void test_QueryTimeoutOnReaderClusterConnection() throws Exception {
try (final Connection conn = connectToInstance(MYSQL_RO_CLUSTER_URL, MYSQL_PORT)) {
assertTrue(conn.isValid(5));
try (final Statement statement = conn.createStatement()) {
statement.setQueryTimeout(1);
statement.execute("SELECT SLEEP(60)");
} catch (MySQLTimeoutException e) {
// ignore
}
}
}
}

0 comments on commit bc6f795

Please sign in to comment.