Skip to content

feat: support MySQL loadbalance:// and replication:// protocols with IAM authentication#1734

Open
sethusrinivasan wants to merge 2 commits into
aws:mainfrom
sethusrinivasan:feature/loadbalance-iam-support
Open

feat: support MySQL loadbalance:// and replication:// protocols with IAM authentication#1734
sethusrinivasan wants to merge 2 commits into
aws:mainfrom
sethusrinivasan:feature/loadbalance-iam-support

Conversation

@sethusrinivasan
Copy link
Copy Markdown

Summary

When using the AWS Advanced JDBC Wrapper with MySQL's multi-host protocols (loadbalance:// or replication://), the wrapper would decompose the multi-host URL into individual single-host connections, breaking the MySQL driver's internal load-balancing and replication routing. Additionally, the JDK dynamic proxy connections created by MySQL's LoadBalancedConnectionProxy caused the wrapper to incorrectly detect connection changes on every statement execution, because Statement.getConnection() returns a different proxy object than the original connection, and the wrapper compared them using object identity (!=).

Description

This change fixes both issues to enable IAM database authentication with MySQL's loadbalance:// protocol against Aurora MySQL clusters.

Code changes (4 files):

  1. Driver.java: Detect multi-host protocols (loadbalance:, replication:) and store the original hosts string in a new ORIGINAL_URL_HOSTS property so it survives the wrapper's URL decomposition into individual HostSpecs.

  2. PropertyDefinition.java: Add ORIGINAL_URL_HOSTS internal property (wrapperOriginalUrlHosts) to carry the multi-host portion through the connection pipeline.

  3. MysqlConnectorJTargetDriverDialect.java: In prepareConnectInfo(), when ORIGINAL_URL_HOSTS is present and the protocol is loadbalance:// or replication://, reconstruct the full multi-host URL instead of using the single HostSpec URL. The property is then stripped by removeAllExcept() so it does not leak to the target driver.

  4. WrapperUtils.java: Add isSameConnection() method used by executeWithPlugins() to replace the != comparison for connection identity checks. Handles three MySQL proxy patterns:

    • Two proxies sharing the same InvocationHandler (same connection)
    • Inner-class proxy detection via this$0 field (JdbcInterfaceProxy referencing its enclosing LoadBalancedConnectionProxy)
    • proxyWraps() checking invokeOn/thisAsConnection fields
    • isWrapperFor fallback in both directions Also changed isWrapperFor catch blocks from SQLException to Exception to handle NullPointerException thrown by JDK dynamic proxies.

Test coverage (3 new test files, 28 tests total):

  1. LoadBalanceProtocolTest.java (10 unit tests):

    • URL parser extracts all hosts from loadbalance:// and replication:// URLs
    • URL parser extracts correct protocol prefix
    • MysqlConnectorJTargetDriverDialect preserves multi-host URL for both protocols
    • Fallback to single-host URL when ORIGINAL_URL_HOSTS is absent
    • Standard protocol ignores ORIGINAL_URL_HOSTS
    • Driver.java host extraction logic for loadbalance and standard URLs
    • CONNECTION_OBJECT_CHANGED detection behavior
  2. WrapperUtilsIsSameConnectionTest.java (10 unit tests):

    • Object identity (same reference)
    • Different non-proxy connections
    • Two proxies with shared/different InvocationHandlers
    • Inner-class proxy detection (simulates MySQL's LoadBalancedConnectionProxy
      • JdbcInterfaceProxy pattern) in both directions
    • isWrapperFor fallback (forward, reverse, exception handling)
    • Proxy vs non-proxy mismatch
  3. LoadBalanceIntegrationTest.java (8 integration tests against live Aurora MySQL):

    • Basic wrapper connection via cluster endpoint
    • loadbalance:// protocol connection with multiple instances
    • 50-query stability test (no false connection-change detection)
    • Load distribution across hosts (verifies >= 2 distinct hosts)
    • Concurrent connection stress test (3 threads x 5 connections)
    • IAM auth + loadbalance:// basic connectivity
    • IAM auth + loadbalance:// multi-query stability
    • IAM auth + loadbalance:// distribution across hosts

All 1794 unit tests pass (0 regressions). All 8 integration tests pass against a live Aurora MySQL cluster with 2 serverless v2 instances. The only failure in the full suite is the pre-existing flaky CustomEndpointMonitorImplTest.testRun().

Co-authored-by: Kiro AI with Claude Opus 4.6

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@davecramer
Copy link
Copy Markdown
Contributor

fixes #1589

…IAM authentication

When using the AWS Advanced JDBC Wrapper with MySQL's multi-host protocols
(loadbalance:// or replication://), the wrapper would decompose the multi-host
URL into individual single-host connections, breaking the MySQL driver's
internal load-balancing and replication routing. Additionally, the JDK dynamic
proxy connections created by MySQL's LoadBalancedConnectionProxy caused the
wrapper to incorrectly detect connection changes on every statement execution,
because Statement.getConnection() returns a different proxy object than the
original connection, and the wrapper compared them using object identity (!=).

This change fixes both issues to enable IAM database authentication with
MySQL's loadbalance:// protocol against Aurora MySQL clusters.

Code changes (4 files):

1. Driver.java: Detect multi-host protocols (loadbalance:, replication:) and
   store the original hosts string in a new ORIGINAL_URL_HOSTS property so
   it survives the wrapper's URL decomposition into individual HostSpecs.

2. PropertyDefinition.java: Add ORIGINAL_URL_HOSTS internal property
   (wrapperOriginalUrlHosts) to carry the multi-host portion through the
   connection pipeline.

3. MysqlConnectorJTargetDriverDialect.java: In prepareConnectInfo(), when
   ORIGINAL_URL_HOSTS is present and the protocol is loadbalance:// or
   replication://, reconstruct the full multi-host URL instead of using the
   single HostSpec URL. The property is then stripped by removeAllExcept()
   so it does not leak to the target driver.

4. WrapperUtils.java: Add isSameConnection() method used by
   executeWithPlugins() to replace the != comparison for connection identity
   checks. Handles three MySQL proxy patterns:
   - Two proxies sharing the same InvocationHandler (same connection)
   - Inner-class proxy detection via this$0 field (JdbcInterfaceProxy
     referencing its enclosing LoadBalancedConnectionProxy)
   - proxyWraps() checking invokeOn/thisAsConnection fields
   - isWrapperFor fallback in both directions
   Also changed isWrapperFor catch blocks from SQLException to Exception
   to handle NullPointerException thrown by JDK dynamic proxies.

Test coverage (3 new test files, 28 tests total):

1. LoadBalanceProtocolTest.java (10 unit tests):
   - URL parser extracts all hosts from loadbalance:// and replication:// URLs
   - URL parser extracts correct protocol prefix
   - MysqlConnectorJTargetDriverDialect preserves multi-host URL for both protocols
   - Fallback to single-host URL when ORIGINAL_URL_HOSTS is absent
   - Standard protocol ignores ORIGINAL_URL_HOSTS
   - Driver.java host extraction logic for loadbalance and standard URLs
   - CONNECTION_OBJECT_CHANGED detection behavior

2. WrapperUtilsIsSameConnectionTest.java (10 unit tests):
   - Object identity (same reference)
   - Different non-proxy connections
   - Two proxies with shared/different InvocationHandlers
   - Inner-class proxy detection (simulates MySQL's LoadBalancedConnectionProxy
     + JdbcInterfaceProxy pattern) in both directions
   - isWrapperFor fallback (forward, reverse, exception handling)
   - Proxy vs non-proxy mismatch

3. LoadBalanceIntegrationTest.java (8 integration tests against live Aurora MySQL):
   - Basic wrapper connection via cluster endpoint
   - loadbalance:// protocol connection with multiple instances
   - 50-query stability test (no false connection-change detection)
   - Load distribution across hosts (verifies >= 2 distinct hosts)
   - Concurrent connection stress test (3 threads x 5 connections)
   - IAM auth + loadbalance:// basic connectivity
   - IAM auth + loadbalance:// multi-query stability
   - IAM auth + loadbalance:// distribution across hosts

All 1794 unit tests pass (0 regressions). All 8 integration tests pass against
a live Aurora MySQL cluster with 2 serverless v2 instances. The only failure in
the full suite is the pre-existing flaky CustomEndpointMonitorImplTest.testRun().

Co-authored-by: Kiro AI with Claude Opus 4.6
@sethusrinivasan sethusrinivasan force-pushed the feature/loadbalance-iam-support branch from d536608 to f38ff44 Compare February 23, 2026 22:01
@davecramer
Copy link
Copy Markdown
Contributor

Reviewed by AI
Issues Found

  1. Host extraction in Driver.java doesn't handle query parameters

final String afterProtocol = driverUrl.substring(targetDriverProtocol.length());
final int endOfHosts = afterProtocol.indexOf('/');

The PR description mentions handling ? and # delimiters, but the code only checks for /. A URL like jdbc:mysql:loadbalance://host1:3306,host2:3306?useSSL=true (no
database path) would include the query string in hostsString. The indexOf('/') would return -1, and the fallback takes the entire afterProtocol including
?useSSL=true.

Suggestion: Also check for ? and #:

int endOfHosts = afterProtocol.length();
for (char c : new char[]{'/', '?', '#'}) {
int idx = afterProtocol.indexOf(c);
if (idx >= 0 && idx < endOfHosts) {
endOfHosts = idx;
}
}
String hostsString = afterProtocol.substring(0, endOfHosts);

  1. isSameConnection uses heavy reflection on every statement execution

The executeWithPlugins method is the hot path — every JDBC call goes through it. The new isSameConnection method uses Proxy.isProxyClass(),
Proxy.getInvocationHandler(), getDeclaredField("this$0"), and setAccessible(true) on every call where conn != currentConnection. For loadbalance connections, this
will be triggered on every statement execution since the proxy objects always differ.

Suggestion: Consider caching the result or at minimum short-circuiting earlier. The Proxy.isProxyClass() check is cheap, but the reflective field access
(getDeclaredField, setAccessible) is not. A WeakHashMap<Pair<Connection,Connection>, Boolean> or even a simple check of Proxy.getInvocationHandler(connA) ==
Proxy.getInvocationHandler(connB) before falling into the expensive inner-class detection would help.

  1. getEnclosingInstance uses setAccessible(true) — may fail under module system

Java 9+ module system restrictions can cause setAccessible(true) to throw InaccessibleObjectException on fields in packages not opened to the unnamed module. MySQL
Connector/J's internal classes may be in a module that doesn't export its internals.

final Field field = obj.getClass().getDeclaredField("this$0");
field.setAccessible(true);

This is caught by the blanket catch (Exception e), so it won't crash, but it means the inner-class detection silently fails and falls through to isWrapperFor, which
may also fail for JDK proxies. Worth documenting this limitation.

  1. proxyWraps also uses setAccessible on MySQL internal fields

Same concern as above — accessing invokeOn and thisAsConnection fields on MySQL Connector/J's internal handler classes. These are private fields in
com.mysql.cj.jdbc.ha.MultiHostConnectionProxy and its inner classes.

  1. Missing blank line before checkThrowable method

In WrapperUtils.java, the new proxyWraps method ends immediately before the existing checkThrowable method with no blank line separator:

  return false;
}
/**
 * Check if the throwable is an instance of the given exception...

Minor style issue but the project uses Google checkstyle which typically requires blank lines between methods.

  1. ORIGINAL_URL_HOSTS is registered as a known wrapper property

Since AwsWrapperProperty fields are auto-registered via registerProperties, ORIGINAL_URL_HOSTS becomes a user-visible property. Users could set
wrapperOriginalUrlHosts in their connection properties and potentially inject arbitrary hosts into the URL. The property description says "Internal:" but there's no
enforcement.

Suggestion: Consider validating that the property wasn't user-supplied, or use a non-AwsWrapperProperty mechanism (e.g., a separate internal-only property key that
doesn't get registered).

  1. Integration test file location

LoadBalanceIntegrationTest.java is placed in wrapper/src/test/java/software/amazon/jdbc/ alongside unit tests. The project likely has a separate source set or
directory convention for integration tests (the test uses @tag("loadbalance-integration")). Verify this matches the project's test organization.

  1. Double database separator possible

In MysqlConnectorJTargetDriverDialect:

urlBuilder = protocol + originalHosts + "/" + databaseName;

If databaseName is empty string (which it can be — the fallback is ""), this produces a trailing / like jdbc:mysql:loadbalance://host1,host2/. The original code path
does protocol + hostSpec.getUrl() + databaseName where hostSpec.getUrl() already includes a /. Verify MySQL Connector/J handles the trailing slash consistently.

Positive Aspects

  • Thorough test coverage: 10 unit tests for protocol handling, 10 for isSameConnection, 8 integration tests
  • Edge cases covered: fallback to single-host, standard protocol ignoring the property, both loadbalance and replication protocols
  • The removeAllExcept in the dialect correctly strips ORIGINAL_URL_HOSTS before passing props to the target driver
  • Good commit message with detailed explanation of the problem and solution
  • The isSameConnection fallback chain (identity → shared handler → inner-class → proxyWraps → isWrapperFor) is well-structured

Verdict

The core approach is correct and well-tested. The main concerns are:

  1. Performance of reflection in the hot path (issue chore(deps): bump commons-dbcp2 from 2.8.0 to 2.9.0 #2) — this needs measurement or mitigation
  2. Host extraction bug with query-string-only URLs (issue chore(deps): bump junit-jupiter-api from 5.8.+ to 5.8.2 #1) — straightforward fix
  3. Security of the user-settable internal property (issue chore(deps): bump rds from 2.17.165 to 2.17.238 #6) — worth hardening

…ve reflection

  Move MySQL-specific connection identity logic out of WrapperUtils and
  into the TargetDriverDialect hierarchy, following the project's existing
  pattern for driver-specific behavior.

  The original isSameConnection() in WrapperUtils used reflection
  (getDeclaredField, setAccessible) to inspect MySQL Connector/J internal
  proxy fields (this$0, invokeOn, thisAsConnection). The new implementation
  uses JdbcConnection.getMultiHostSafeProxy(), a public MySQL API that
  returns the same stable proxy object for both the original loadbalance
  connection and any JdbcInterfaceProxy returned by Statement.getConnection().

  Changes:
  - TargetDriverDialect: add default isSameConnection() (identity check)
  - MysqlConnectorJTargetDriverDialect: override to delegate to helper
  - MysqlConnectorJDriverHelper: implement using JdbcConnection API, no reflection
  - WrapperUtils: remove isSameConnection/getEnclosingInstance/proxyWraps
    (136 lines), call dialect.isSameConnection() from executeWithPlugins
  - WrapperUtilsTest: wire up GenericTargetDriverDialect mock
  - Replace WrapperUtilsIsSameConnectionTest with IsSameConnectionTest
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants