Skip to content

Commit c58d3d2

Browse files
authored
Allow optional failover in the connect pipeline (#1099)
1 parent 3861578 commit c58d3d2

File tree

8 files changed

+355
-10
lines changed

8 files changed

+355
-10
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
33

44
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).
55

6+
### :magic_wand: Added
7+
- 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]
8+
69
## [2.3.9] - 2024-08-09
710

811
### :bug: Fixed

docs/using-the-jdbc-driver/using-plugins/UsingTheFailoverPlugin.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ In addition to the parameters that you can configure for the underlying driver,
3030
| `failoverReaderConnectTimeoutMs` | Integer | No | Maximum allowed time in milliseconds to attempt to connect to a reader instance during a reader failover process. | `30000` |
3131
| `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` |
3232
| `failoverWriterReconnectIntervalMs` | Integer | No | Interval of time in milliseconds to wait between attempts to reconnect to a failed writer during a writer failover process. | `2000` |
33+
| `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` |
3334
| ~~`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` |
3435
| ~~`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. | |
3536

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package software.amazon.jdbc;
18+
19+
import java.util.Properties;
20+
21+
@FunctionalInterface
22+
public interface AcceptsUrlFunc {
23+
24+
/**
25+
* This function can be passed to a {@link ConnectionProvider} constructor to specify when the
26+
* {@link ConnectionProvider} should be used to open a connection to the given {@link HostSpec} with the
27+
* given {@link Properties}.
28+
*
29+
* @param hostSpec the host details for the requested connection
30+
* @param props the properties for the requested connection
31+
* @return a boolean indicating whether a {@link ConnectionProvider} should be used to open a connection to the given
32+
* {@link HostSpec} with the given {@link Properties}.
33+
*/
34+
boolean acceptsUrl(HostSpec hostSpec, Properties props);
35+
}

wrapper/src/main/java/software/amazon/jdbc/HikariPooledConnectionProvider.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public class HikariPooledConnectionProvider implements PooledConnectionProvider,
6969
private static long poolExpirationCheckNanos = TimeUnit.MINUTES.toNanos(30);
7070
private final HikariPoolConfigurator poolConfigurator;
7171
private final HikariPoolMapping poolMapping;
72+
private final AcceptsUrlFunc acceptsUrlFunc;
7273
private final LeastConnectionsHostSelector leastConnectionsHostSelector;
7374

7475
/**
@@ -112,6 +113,7 @@ public HikariPooledConnectionProvider(
112113
HikariPoolConfigurator hikariPoolConfigurator, HikariPoolMapping mapping) {
113114
this.poolConfigurator = hikariPoolConfigurator;
114115
this.poolMapping = mapping;
116+
this.acceptsUrlFunc = null;
115117
this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools);
116118
}
117119

@@ -147,14 +149,62 @@ public HikariPooledConnectionProvider(
147149
long poolCleanupNanos) {
148150
this.poolConfigurator = hikariPoolConfigurator;
149151
this.poolMapping = mapping;
152+
this.acceptsUrlFunc = null;
150153
poolExpirationCheckNanos = poolExpirationNanos;
151154
databasePools.setCleanupIntervalNanos(poolCleanupNanos);
152155
this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools);
153156
}
154157

158+
/**
159+
* {@link HikariPooledConnectionProvider} constructor. This class can be passed to
160+
* {@link ConnectionProviderManager#setConnectionProvider} to enable internal connection pools for
161+
* each database instance in a cluster. By maintaining internal connection pools, the driver can
162+
* improve performance by reusing old {@link Connection} objects.
163+
*
164+
* @param hikariPoolConfigurator a function that returns a {@link HikariConfig} with specific
165+
* Hikari configurations. By default, the
166+
* {@link HikariPooledConnectionProvider} will configure the
167+
* jdbcUrl, exceptionOverrideClassName, username, and password. Any
168+
* additional configuration should be defined by passing in this
169+
* parameter. If no additional configuration is desired, pass in a
170+
* {@link HikariPoolConfigurator} that returns an empty
171+
* HikariConfig.
172+
* @param mapping a function that returns a String key used for the internal
173+
* connection pool keys. An internal connection pool will be
174+
* generated for each unique key returned by this function.
175+
* @param acceptsUrlFunc a function that defines when an internal connection pool should be created for a
176+
* requested connection. An internal connection pool will be created when the connect
177+
* pipeline is being executed and this function returns <code>true</code>.
178+
* @param poolExpirationNanos the amount of time that a pool should sit in the cache before
179+
* being marked as expired for cleanup, in nanoseconds. Expired
180+
* pools can still be used and will not be closed unless there
181+
* are no active connections.
182+
* @param poolCleanupNanos the interval defining how often expired connection pools
183+
* should be cleaned up, in nanoseconds. Note that expired pools
184+
* will not be closed unless there are no active connections.
185+
*/
186+
public HikariPooledConnectionProvider(
187+
HikariPoolConfigurator hikariPoolConfigurator,
188+
HikariPoolMapping mapping,
189+
AcceptsUrlFunc acceptsUrlFunc,
190+
long poolExpirationNanos,
191+
long poolCleanupNanos) {
192+
this.poolConfigurator = hikariPoolConfigurator;
193+
this.poolMapping = mapping;
194+
this.acceptsUrlFunc = acceptsUrlFunc;
195+
poolExpirationCheckNanos = poolExpirationNanos;
196+
databasePools.setCleanupIntervalNanos(poolCleanupNanos);
197+
this.leastConnectionsHostSelector = new LeastConnectionsHostSelector(databasePools);
198+
}
199+
200+
155201
@Override
156202
public boolean acceptsUrl(
157203
@NonNull String protocol, @NonNull HostSpec hostSpec, @NonNull Properties props) {
204+
if (this.acceptsUrlFunc != null) {
205+
return this.acceptsUrlFunc.acceptsUrl(hostSpec, props);
206+
}
207+
158208
final RdsUrlType urlType = rdsUtils.identifyRdsType(hostSpec.getHost());
159209
return RdsUrlType.RDS_INSTANCE.equals(urlType);
160210
}

wrapper/src/main/java/software/amazon/jdbc/exceptions/MySQLExceptionHandler.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,29 @@
2121

2222
public class MySQLExceptionHandler implements ExceptionHandler {
2323
public static final String SQLSTATE_ACCESS_ERROR = "28000";
24+
public static final String SQLSTATE_SYNTAX_ERROR_OR_ACCESS_VIOLATION = "42000";
25+
public static final String SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION =
26+
"setNetworkTimeout cannot be called on a closed connection";
2427

2528
@Override
2629
public boolean isNetworkException(final Throwable throwable) {
2730
Throwable exception = throwable;
2831

2932
while (exception != null) {
3033
if (exception instanceof SQLException) {
31-
return isNetworkException(((SQLException) exception).getSQLState());
34+
SQLException sqlException = (SQLException) exception;
35+
36+
// Hikari throws a network exception with SQL state 42000 if all the following points are true:
37+
// - HikariDataSource#getConnection is called and the cached connection that was grabbed is broken due to server
38+
// failover.
39+
// - the MariaDB driver is being used (the underlying driver determines the SQL state of the Hikari exception).
40+
//
41+
// The check for the Hikari MariaDB exception is added here because the exception handler is determined by the
42+
// database dialect. Consequently, this exception handler is used when using the MariaDB driver against a MySQL
43+
// database engine.
44+
if (isNetworkException(sqlException.getSQLState()) || isHikariMariaDbNetworkException(sqlException)) {
45+
return true;
46+
}
3247
} else if (exception instanceof CJException) {
3348
return isNetworkException(((CJException) exception).getSQLState());
3449
}
@@ -76,4 +91,9 @@ public boolean isLoginException(final String sqlState) {
7691

7792
return SQLSTATE_ACCESS_ERROR.equals(sqlState);
7893
}
94+
95+
private boolean isHikariMariaDbNetworkException(final SQLException sqlException) {
96+
return sqlException.getSQLState().equals(SQLSTATE_SYNTAX_ERROR_OR_ACCESS_VIOLATION)
97+
&& sqlException.getMessage().contains(SET_NETWORK_TIMEOUT_ON_CLOSED_CONNECTION);
98+
}
7999
}

wrapper/src/main/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPlugin.java

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin {
8484
private final PluginService pluginService;
8585
protected final Properties properties;
8686
protected boolean enableFailoverSetting;
87+
protected boolean enableConnectFailover;
8788
protected int failoverTimeoutMsSetting;
8889
protected int failoverClusterTopologyRefreshRateMsSetting;
8990
protected int failoverWriterReconnectIntervalMsSetting;
@@ -137,6 +138,13 @@ public class FailoverConnectionPlugin extends AbstractConnectionPlugin {
137138
"enableClusterAwareFailover", "true",
138139
"Enable/disable cluster-aware failover logic");
139140

141+
public static final AwsWrapperProperty ENABLE_CONNECT_FAILOVER =
142+
new AwsWrapperProperty(
143+
"enableConnectFailover", "false",
144+
"Enable/disable cluster-aware failover if the initial connection to the database fails due to a "
145+
+ "network exception. Note that this may result in a connection to a different instance in the cluster "
146+
+ "than was specified by the URL.");
147+
140148
public static final AwsWrapperProperty FAILOVER_MODE =
141149
new AwsWrapperProperty(
142150
"failoverMode", null,
@@ -353,6 +361,7 @@ public boolean isFailoverEnabled() {
353361

354362
private void initSettings() {
355363
this.enableFailoverSetting = ENABLE_CLUSTER_AWARE_FAILOVER.getBoolean(this.properties);
364+
this.enableConnectFailover = ENABLE_CONNECT_FAILOVER.getBoolean(this.properties);
356365
this.failoverTimeoutMsSetting = FAILOVER_TIMEOUT_MS.getInteger(this.properties);
357366
this.failoverClusterTopologyRefreshRateMsSetting =
358367
FAILOVER_CLUSTER_TOPOLOGY_REFRESH_RATE_MS.getInteger(this.properties);
@@ -767,15 +776,34 @@ public Connection connect(
767776
final boolean isInitialConnection,
768777
final JdbcCallable<Connection, SQLException> connectFunc)
769778
throws SQLException {
770-
return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, connectFunc);
779+
return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, connectFunc, false);
771780
}
772781

773782
private Connection connectInternal(String driverProtocol, HostSpec hostSpec, Properties props,
774-
boolean isInitialConnection, JdbcCallable<Connection, SQLException> connectFunc)
783+
boolean isInitialConnection, JdbcCallable<Connection, SQLException> connectFunc, boolean isForceConnect)
775784
throws SQLException {
776-
final Connection conn =
777-
this.staleDnsHelper.getVerifiedConnection(isInitialConnection, this.hostListProviderService,
778-
driverProtocol, hostSpec, props, connectFunc);
785+
786+
Connection conn = null;
787+
try {
788+
conn =
789+
this.staleDnsHelper.getVerifiedConnection(isInitialConnection, this.hostListProviderService,
790+
driverProtocol, hostSpec, props, connectFunc);
791+
} catch (final SQLException e) {
792+
if (!this.enableConnectFailover || isForceConnect || !shouldExceptionTriggerConnectionSwitch(e)) {
793+
throw e;
794+
}
795+
796+
try {
797+
failover(this.pluginService.getCurrentHostSpec());
798+
} catch (FailoverSuccessSQLException failoverSuccessException) {
799+
conn = this.pluginService.getCurrentConnection();
800+
}
801+
}
802+
803+
if (conn == null) {
804+
// This should be unreachable, the above logic will either get a connection successfully or throw an exception.
805+
throw new SQLException(Messages.get("Failover.unableToConnect"));
806+
}
779807

780808
if (isInitialConnection) {
781809
this.pluginService.refreshHostList(conn);
@@ -792,6 +820,6 @@ public Connection forceConnect(
792820
final boolean isInitialConnection,
793821
final JdbcCallable<Connection, SQLException> forceConnectFunc)
794822
throws SQLException {
795-
return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, forceConnectFunc);
823+
return connectInternal(driverProtocol, hostSpec, props, isInitialConnection, forceConnectFunc, true);
796824
}
797825
}

0 commit comments

Comments
 (0)