diff --git a/CHANGELOG.md b/CHANGELOG.md index b97d3b0e4..5cc432412 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/#semantic-versioning-200). +### :magic_wand: Added +- Logic and a connection property to enable driver failover when network exceptions occur in the connect pipeline (PR #1099)[https://github.com/aws/aws-advanced-jdbc-wrapper/pull/1099] + ## [2.3.9] - 2024-08-09 ### :bug: Fixed diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md index 6de99f258..aa925fcb1 100644 --- a/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md +++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md @@ -30,6 +30,7 @@ In addition to the parameters that you can configure for the underlying driver, | `failoverReaderConnectTimeoutMs` | Integer | No | Maximum allowed time in milliseconds to attempt to connect to a reader instance during a reader failover process. | `30000` | | `failoverTimeoutMs` | Integer | No | Maximum allowed time in milliseconds to attempt reconnecting to a new writer or reader instance after a cluster failover is initiated. | `300000` | | `failoverWriterReconnectIntervalMs` | Integer | No | Interval of time in milliseconds to wait between attempts to reconnect to a failed writer during a writer failover process. | `2000` | +| `enableConnectFailover` | Boolean | No | Enables/disables cluster-aware failover if the initial connection to the database fails due to a network exception. Note that this may result in a connection to a different instance in the cluster than was specified by the URL. | `false` | | ~~`keepSessionStateOnFailover`~~ | Boolean | No | This parameter is no longer available. If specified, it will be ignored by the driver. See [Session State](../SessionState.md) for more details. | `false` | | ~~`enableFailoverStrictReader`~~ | Boolean | No | This parameter is no longer available and, if specified, it will be ignored by the driver. See `failoverMode` (`reader-or-writer` or `strict-reader`) for more details. | | diff --git a/wrapper/src/main/java/software/amazon/jdbc/AcceptsUrlFunc.java b/wrapper/src/main/java/software/amazon/jdbc/AcceptsUrlFunc.java new file mode 100644 index 000000000..65f84c513 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/AcceptsUrlFunc.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package software.amazon.jdbc; + +import java.util.Properties; + +@FunctionalInterface +public interface AcceptsUrlFunc { + + /** + * This function can be passed to a {@link ConnectionProvider} constructor to specify when the + * {@link ConnectionProvider} should be used to open a connection to the given {@link HostSpec} with the + * given {@link Properties}. + * + * @param hostSpec the host details for the requested connection + * @param props the properties for the requested connection + * @return a boolean indicating whether a {@link ConnectionProvider} should be used to open a connection to the given + * {@link HostSpec} with the given {@link Properties}. + */ + boolean acceptsUrl(HostSpec hostSpec, Properties props); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java b/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java index 0dde3e47e..8c2cd1e13 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java @@ -69,6 +69,7 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider, private static long poolExpirationCheckNanos = TimeUnit.MINUTES.toNanos(30); private final HikariPoolConfigurator poolConfigurator; private final HikariPoolMapping poolMapping; + private final AcceptsUrlFunc acceptsUrlFunc; private final LeastConnectionsHostSelector leastConnectionsHostSelector; /** @@ -112,6 +113,7 @@ public HikariPooledConnectionProvider( HikariPoolConfigurator hikariPoolConfigurator, HikariPoolMapping mapping) { this.poolConfigurator = hikariPoolConfigurator; this.poolMapping = mapping; + this.acceptsUrlFunc = null; this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools); } @@ -147,14 +149,62 @@ public HikariPooledConnectionProvider( long poolCleanupNanos) { this.poolConfigurator = hikariPoolConfigurator; this.poolMapping = mapping; + this.acceptsUrlFunc = null; poolExpirationCheckNanos = poolExpirationNanos; databasePools.setCleanupIntervalNanos(poolCleanupNanos); this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools); } + /** + * {@link HikariPooledConnectionProvider} constructor. This class can be passed to + * {@link ConnectionProviderManager#setConnectionProvider} to enable internal connection pools for + * each database instance in a cluster. By maintaining internal connection pools, the driver can + * improve performance by reusing old {@link Connection} objects. + * + * @param hikariPoolConfigurator a function that returns a {@link HikariConfig} with specific + * Hikari configurations. By default, the + * {@link HikariPooledConnectionProvider} will configure the + * jdbcUrl, exceptionOverrideClassName, username, and password. Any + * additional configuration should be defined by passing in this + * parameter. If no additional configuration is desired, pass in a + * {@link HikariPoolConfigurator} that returns an empty + * HikariConfig. + * @param mapping a function that returns a String key used for the internal + * connection pool keys. An internal connection pool will be + * generated for each unique key returned by this function. + * @param acceptsUrlFunc a function that defines when an internal connection pool should be created for a + * requested connection. An internal connection pool will be created when the connect + * pipeline is being executed and this function returns true. + * @param poolExpirationNanos the amount of time that a pool should sit in the cache before + * being marked as expired for cleanup, in nanoseconds. Expired + * pools can still be used and will not be closed unless there + * are no active connections. + * @param poolCleanupNanos the interval defining how often expired connection pools + * should be cleaned up, in nanoseconds. Note that expired pools + * will not be closed unless there are no active connections. + */ + public HikariPooledConnectionProvider( + HikariPoolConfigurator hikariPoolConfigurator, + HikariPoolMapping mapping, + AcceptsUrlFunc acceptsUrlFunc, + long poolExpirationNanos, + long poolCleanupNanos) { + this.poolConfigurator = hikariPoolConfigurator; + this.poolMapping = mapping; + this.acceptsUrlFunc = acceptsUrlFunc; + poolExpirationCheckNanos = poolExpirationNanos; + databasePools.setCleanupIntervalNanos(poolCleanupNanos); + this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools); + } + + @Override public boolean acceptsUrl( @NonNull String protocol, @NonNull HostSpec hostSpec, @NonNull Properties props) { + if (this.acceptsUrlFunc != null) { + return this.acceptsUrlFunc.acceptsUrl(hostSpec, props); + } + final RdsUrlType urlType = rdsUtils.identifyRdsType(hostSpec.getHost()); return RdsUrlType.RDS_INSTANCE.equals(urlType); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java b/wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java index 65c9701e6..c6d7aeee1 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java +++ b/wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java @@ -21,6 +21,9 @@ public class MySQLExceptionHandler implements ExceptionHandler { public static final String SQLSTATE_ACCESS_ERROR = "28000"; + public static final String SQLSTATE_SYNTAX_ERROR_OR_ACCESS_VIOLATION = "42000"; + public static final String SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION = + "setNetworkTimeout cannot be called on a closed connection"; @Override public boolean isNetworkException(final Throwable throwable) { @@ -28,7 +31,19 @@ public boolean isNetworkException(final Throwable throwable) { while (exception != null) { if (exception instanceof SQLException) { - return isNetworkException(((SQLException) exception).getSQLState()); + SQLException sqlException = (SQLException) exception; + + // Hikari throws a network exception with SQL state 42000 if all the following points are true: + // - HikariDataSource#getConnection is called and the cached connection that was grabbed is broken due to server + // failover. + // - the MariaDB driver is being used (the underlying driver determines the SQL state of the Hikari exception). + // + // The check for the Hikari MariaDB exception is added here because the exception handler is determined by the + // database dialect. Consequently, this exception handler is used when using the MariaDB driver against a MySQL + // database engine. + if (isNetworkException(sqlException.getSQLState()) || isHikariMariaDbNetworkException(sqlException)) { + return true; + } } else if (exception instanceof CJException) { return isNetworkException(((CJException) exception).getSQLState()); } @@ -76,4 +91,9 @@ public boolean isLoginException(final String sqlState) { return SQLSTATE_ACCESS_ERROR.equals(sqlState); } + + private boolean isHikariMariaDbNetworkException(final SQLException sqlException) { + return sqlException.getSQLState().equals(SQLSTATE_SYNTAX_ERROR_OR_ACCESS_VIOLATION) + && sqlException.getMessage().contains(SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION); + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java index 2cd76f5ed..15fb51ca8 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java @@ -84,6 +84,7 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { private final PluginService pluginService; protected final Properties properties; protected boolean enableFailoverSetting; + protected boolean enableConnectFailover; protected int failoverTimeoutMsSetting; protected int failoverClusterTopologyRefreshRateMsSetting; protected int failoverWriterReconnectIntervalMsSetting; @@ -137,6 +138,13 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin { "enableClusterAwareFailover", "true", "Enable/disable cluster-aware failover logic"); + public static final AwsWrapperProperty ENABLE_CONNECT_FAILOVER = + new AwsWrapperProperty( + "enableConnectFailover", "false", + "Enable/disable cluster-aware failover if the initial connection to the database fails due to a " + + "network exception. Note that this may result in a connection to a different instance in the cluster " + + "than was specified by the URL."); + public static final AwsWrapperProperty FAILOVER_MODE = new AwsWrapperProperty( "failoverMode", null, @@ -353,6 +361,7 @@ public boolean isFailoverEnabled() { private void initSettings() { this.enableFailoverSetting = ENABLE_CLUSTER_AWARE_FAILOVER.getBoolean(this.properties); + this.enableConnectFailover = ENABLE_CONNECT_FAILOVER.getBoolean(this.properties); this.failoverTimeoutMsSetting = FAILOVER_TIMEOUT_MS.getInteger(this.properties); this.failoverClusterTopologyRefreshRateMsSetting = FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS.getInteger(this.properties); @@ -767,15 +776,34 @@ public Connection connect( final boolean isInitialConnection, final JdbcCallable connectFunc) throws SQLException { - return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, connectFunc); + return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, connectFunc, false); } private Connection connectInternal(String driverProtocol, HostSpec hostSpec, Properties props, - boolean isInitialConnection, JdbcCallable connectFunc) + boolean isInitialConnection, JdbcCallable connectFunc, boolean isForceConnect) throws SQLException { - final Connection conn = - this.staleDnsHelper.getVerifiedConnection(isInitialConnection, this.hostListProviderService, - driverProtocol, hostSpec, props, connectFunc); + + Connection conn = null; + try { + conn = + this.staleDnsHelper.getVerifiedConnection(isInitialConnection, this.hostListProviderService, + driverProtocol, hostSpec, props, connectFunc); + } catch (final SQLException e) { + if (!this.enableConnectFailover || isForceConnect || !shouldExceptionTriggerConnectionSwitch(e)) { + throw e; + } + + try { + failover(this.pluginService.getCurrentHostSpec()); + } catch (FailoverSuccessSQLException failoverSuccessException) { + conn = this.pluginService.getCurrentConnection(); + } + } + + if (conn == null) { + // This should be unreachable, the above logic will either get a connection successfully or throw an exception. + throw new SQLException(Messages.get("Failover.unableToConnect")); + } if (isInitialConnection) { this.pluginService.refreshHostList(conn); @@ -792,6 +820,6 @@ public Connection forceConnect( final boolean isInitialConnection, final JdbcCallable forceConnectFunc) throws SQLException { - return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, forceConnectFunc); + return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, forceConnectFunc, true); } } diff --git a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties index 7e7da6cb2..13f5579a4 100644 --- a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties +++ b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties @@ -147,6 +147,7 @@ ExecutionTimeConnectionPlugin.executionTime=Executed {0} in {1} nanos. Failover.transactionResolutionUnknownError=Transaction resolution unknown. Please re-configure session state if required and try restarting the transaction. Failover.connectionChangedError=The active SQL connection has changed due to a connection failure. Please re-configure session state if required. Failover.parameterValue={0}={1} +Failover.unableToConnect=Unable to establish a SQL connection due to an unexpected error. Failover.unableToConnectToWriter=Unable to establish SQL connection to the writer instance. Failover.unableToConnectToReader=Unable to establish SQL connection to the reader instance. Failover.detectedException=Detected an exception while executing a command: {0} diff --git a/wrapper/src/test/java/integration/container/tests/HikariTests.java b/wrapper/src/test/java/integration/container/tests/HikariTests.java index 4ac033f45..e1290c44e 100644 --- a/wrapper/src/test/java/integration/container/tests/HikariTests.java +++ b/wrapper/src/test/java/integration/container/tests/HikariTests.java @@ -17,7 +17,9 @@ package integration.container.tests; import static integration.util.AuroraTestUtility.executeWithTimeout; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static software.amazon.jdbc.PropertyDefinition.PLUGINS; @@ -47,6 +49,7 @@ import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTransientConnectionException; +import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.Properties; @@ -57,8 +60,13 @@ import org.junit.jupiter.api.TestMethodOrder; import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; +import software.amazon.jdbc.AcceptsUrlFunc; +import software.amazon.jdbc.ConnectionProviderManager; +import software.amazon.jdbc.HikariPoolConfigurator; +import software.amazon.jdbc.HikariPooledConnectionProvider; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.ds.AwsWrapperDataSource; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.efm2.HostMonitoringConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverFailedSQLException; @@ -173,7 +181,7 @@ public void testFailoverLostConnection() throws SQLException { FAILOVER_TIMEOUT_MS.set(customProps, Integer.toString(1)); PropertyDefinition.SOCKET_TIMEOUT.set(customProps, String.valueOf(TimeUnit.SECONDS.toMillis(1))); - try (final HikariDataSource dataSource = createDataSource(customProps)) { + try (final HikariDataSource dataSource = createHikariDataSource(customProps)) { try (Connection conn = dataSource.getConnection()) { assertTrue(conn.isValid(5)); @@ -224,7 +232,7 @@ public void testEFMFailover() throws SQLException { LOGGER.fine("Instance to fail over to: " + readerIdentifier); ProxyHelper.enableConnectivity(writerIdentifier); - try (final HikariDataSource dataSource = createDataSource(null)) { + try (final HikariDataSource dataSource = createHikariDataSource(null)) { // Get a valid connection, then make it fail over to a different instance try (Connection conn = dataSource.getConnection()) { @@ -252,7 +260,206 @@ public void testEFMFailover() throws SQLException { } } - private HikariDataSource createDataSource(final Properties customProps) { + /** + * After successfully opening and returning a connection to the Hikari pool, writer failover is triggered when + * getConnection is called. + */ + @TestTemplate + @EnableOnDatabaseEngineDeployment(DatabaseEngineDeployment.AURORA) + @EnableOnTestFeature({TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED}) + @EnableOnNumOfInstances(min = 2) + public void testInternalPools_driverWriterFailoverOnGetConnectionInvocation() + throws SQLException, InterruptedException { + final AuroraTestUtility auroraUtil = AuroraTestUtility.getUtility(); + final TestProxyDatabaseInfo proxyInfo = TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo(); + final List instances = proxyInfo.getInstances(); + final TestInstanceInfo reader = instances.get(1); + final String readerId = reader.getInstanceId(); + + setupInternalConnectionPools(getInstanceUrlSubstring(reader.getHost())); + try { + final Properties targetDataSourceProps = new Properties(); + targetDataSourceProps.setProperty(FailoverConnectionPlugin.FAILOVER_MODE.name, "strict-writer"); + final AwsWrapperDataSource ds = createWrapperDataSource(reader, proxyInfo, targetDataSourceProps); + + // Open connection and then return it to the pool + Connection conn = ds.getConnection(); + assertEquals(readerId, auroraUtil.queryInstanceId(conn)); + conn.close(); + + ProxyHelper.disableConnectivity(reader.getInstanceId()); + // Hikari's 'com.zaxxer.hikari.aliveBypassWindowMs' property is set to 500ms by default. We need to wait this long + // to trigger Hikari's validation attempts when HikariDatasource#getConnection is called. These attempts will fail + // and Hikari will throw an exception, which should trigger failover. + TimeUnit.MILLISECONDS.sleep(500); + + // Driver will fail over internally and return a connection to another node. + conn = ds.getConnection(); + // Assert that we connected to a different node. + assertNotEquals(readerId, auroraUtil.queryInstanceId(conn)); + } finally { + ConnectionProviderManager.releaseResources(); + } + } + + /** + * After successfully opening and returning connections to the Hikari pool, reader failover is triggered when + * getConnection is called. + */ + @TestTemplate + @EnableOnDatabaseEngineDeployment(DatabaseEngineDeployment.AURORA) + @EnableOnTestFeature({TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED}) + @EnableOnNumOfInstances(min = 2) + public void testInternalPools_driverReaderFailoverOnGetConnectionInvocation() + throws SQLException, InterruptedException { + final AuroraTestUtility auroraUtil = AuroraTestUtility.getUtility(); + final TestProxyDatabaseInfo proxyInfo = TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo(); + final List instances = proxyInfo.getInstances(); + final TestInstanceInfo writer = instances.get(0); + final String writerId = writer.getInstanceId(); + + setupInternalConnectionPools(getInstanceUrlSubstring(writer.getHost())); + try { + final Properties targetDataSourceProps = new Properties(); + targetDataSourceProps.setProperty(FailoverConnectionPlugin.FAILOVER_MODE.name, "strict-reader"); + final AwsWrapperDataSource ds = createWrapperDataSource(writer, proxyInfo, targetDataSourceProps); + + // Open some connections. + List connections = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + Connection conn = ds.getConnection(); + connections.add(conn); + } + + // Return the opened connections to the pool. + for (Connection conn : connections) { + conn.close(); + } + + ProxyHelper.disableConnectivity(writer.getInstanceId()); + // Hikari's 'com.zaxxer.hikari.aliveBypassWindowMs' property is set to 500ms by default. We need to wait this long + // to trigger Hikari's validation attempts when HikariDatasource#getConnection is called. These attempts will fail + // and Hikari will throw an exception, which should trigger failover. + TimeUnit.MILLISECONDS.sleep(500); + + // Driver will fail over internally and return a connection to another node. + try (Connection conn = ds.getConnection()) { + // Assert that we connected to a different node. + assertNotEquals(writerId, auroraUtil.queryInstanceId(conn)); + } + } finally { + ConnectionProviderManager.releaseResources(); + } + } + + /** + * After successfully opening and returning a connection to the Hikari pool, writer failover is triggered when + * getConnection is called. Since the cluster only has one instance and the instance stays down, failover fails. + */ + @TestTemplate + @EnableOnDatabaseEngineDeployment(DatabaseEngineDeployment.AURORA) + @EnableOnTestFeature({TestEnvironmentFeatures.NETWORK_OUTAGES_ENABLED}) + @EnableOnNumOfInstances(max = 1) + public void testInternalPools_driverWriterFailoverOnGetConnectionInvocation_singleInstance() + throws SQLException, InterruptedException { + final AuroraTestUtility auroraUtil = AuroraTestUtility.getUtility(); + final TestProxyDatabaseInfo proxyInfo = TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo(); + final List instances = proxyInfo.getInstances(); + final TestInstanceInfo writer = instances.get(0); + final String writerId = writer.getInstanceId(); + + setupInternalConnectionPools(getInstanceUrlSubstring(writer.getHost())); + try { + final Properties targetDataSourceProps = new Properties(); + targetDataSourceProps.setProperty(FailoverConnectionPlugin.FAILOVER_MODE.name, "strict-writer"); + targetDataSourceProps.setProperty(FailoverConnectionPlugin.FAILOVER_TIMEOUT_MS.name, "5000"); + final AwsWrapperDataSource ds = createWrapperDataSource(writer, proxyInfo, targetDataSourceProps); + + // Open connection and then return it to the pool + Connection conn = ds.getConnection(); + assertEquals(writerId, auroraUtil.queryInstanceId(conn)); + conn.close(); + + ProxyHelper.disableAllConnectivity(); + // Hikari's 'com.zaxxer.hikari.aliveBypassWindowMs' property is set to 500ms by default. We need to wait this long + // to trigger Hikari's validation attempts when HikariDatasource#getConnection is called. These attempts will fail + // and Hikari will throw an exception, which should trigger failover. + TimeUnit.MILLISECONDS.sleep(500); + + // Driver will attempt to fail over internally, but the node is still down, so it fails. + assertThrows(FailoverFailedSQLException.class, ds::getConnection); + } finally { + ConnectionProviderManager.releaseResources(); + } + } + + /** + * Given an instance URL, extracts the substring of the URL that is common to all instance URLs. For example, given + * "instance-1.ABC.cluster-XYZ.us-west-2.rds.amazonaws.com.proxied", returns + * ".ABC.cluster-XYZ.us-west-2.rds.amazonaws.com.proxied" + */ + private String getInstanceUrlSubstring(String instanceUrl) { + int substringStart = instanceUrl.indexOf("."); + return instanceUrl.substring(substringStart); + } + + private AwsWrapperDataSource createWrapperDataSource(TestInstanceInfo instanceInfo, + TestProxyDatabaseInfo proxyInfo, Properties targetDataSourceProps) { + targetDataSourceProps.setProperty(PropertyDefinition.PLUGINS.name, "auroraConnectionTracker,failover,efm"); + targetDataSourceProps.setProperty(FailoverConnectionPlugin.ENABLE_CONNECT_FAILOVER.name, "true"); + targetDataSourceProps.setProperty(RdsHostListProvider.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.name, + String.valueOf(TimeUnit.MINUTES.toMillis(5))); + targetDataSourceProps.setProperty(RdsHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.name, + "?." + proxyInfo.getInstanceEndpointSuffix()); + targetDataSourceProps.setProperty(RdsHostListProvider.CLUSTER_ID.name, "HikariTestsCluster"); + + if (TestEnvironment.getCurrent().getCurrentDriver() == TestDriver.MARIADB + && TestEnvironment.getCurrent().getInfo().getRequest().getDatabaseEngine() == DatabaseEngine.MYSQL) { + targetDataSourceProps.setProperty("permitMysqlScheme", "1"); + } + + AwsWrapperDataSource ds = new AwsWrapperDataSource(); + ds.setTargetDataSourceProperties(targetDataSourceProps); + ds.setJdbcProtocol(DriverHelper.getDriverProtocol()); + ds.setTargetDataSourceClassName(DriverHelper.getDataSourceClassname()); + ds.setServerName(instanceInfo.getHost()); + ds.setServerPort(String.valueOf(instanceInfo.getPort())); + ds.setDatabase(proxyInfo.getDefaultDbName()); + ds.setUser(proxyInfo.getUsername()); + ds.setPassword(proxyInfo.getPassword()); + + return ds; + } + + private void setupInternalConnectionPools(String instanceUrlSubstring) { + HikariPoolConfigurator hikariConfigurator = (hostSpec, props) -> { + HikariConfig config = new HikariConfig(); + config.setMaximumPoolSize(30); + config.setMinimumIdle(2); + config.setIdleTimeout(TimeUnit.MINUTES.toMillis(15)); + config.setInitializationFailTimeout(-1); + config.setConnectionTimeout(1500); + config.setKeepaliveTime(TimeUnit.MINUTES.toMillis(3)); + config.setValidationTimeout(1000); + config.setMaxLifetime(TimeUnit.DAYS.toMillis(1)); + config.setReadOnly(true); + config.setAutoCommit(true); + return config; + }; + + AcceptsUrlFunc acceptsUrlFunc = (hostSpec, props) -> hostSpec.getHost().contains(instanceUrlSubstring); + + final HikariPooledConnectionProvider provider = + new HikariPooledConnectionProvider( + hikariConfigurator, + null, + acceptsUrlFunc, + TimeUnit.MINUTES.toNanos(30), + TimeUnit.MINUTES.toNanos(10)); + ConnectionProviderManager.setConnectionProvider(provider); + } + + private HikariDataSource createHikariDataSource(final Properties customProps) { final HikariConfig config = getConfig(customProps); final HikariDataSource dataSource = new HikariDataSource(config);