diff --git a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java index eb4a8d3b2..ebdb34355 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/ConnectionPluginManagerBenchmarks.java @@ -54,7 +54,7 @@ import software.amazon.jdbc.ConnectionPluginFactory; import software.amazon.jdbc.ConnectionPluginManager; import software.amazon.jdbc.ConnectionProvider; -import software.amazon.jdbc.HostListProviderService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.JdbcMethod; diff --git a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java index 405b6fc00..dda847f73 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/PluginBenchmarks.java @@ -54,7 +54,7 @@ import software.amazon.jdbc.ConnectionProviderManager; import software.amazon.jdbc.Driver; import software.amazon.jdbc.HikariPooledConnectionProvider; -import software.amazon.jdbc.HostListProviderService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.JdbcMethod; diff --git a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/BenchmarkPlugin.java b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/BenchmarkPlugin.java index ffed10f77..07fede27a 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/BenchmarkPlugin.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/BenchmarkPlugin.java @@ -28,7 +28,7 @@ import java.util.Set; import java.util.logging.Logger; import software.amazon.jdbc.ConnectionPlugin; -import software.amazon.jdbc.HostListProviderService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; diff --git a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/TestConnectionWrapper.java b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/TestConnectionWrapper.java index a3c0cd7f2..483d6768c 100644 --- a/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/TestConnectionWrapper.java +++ b/benchmarks/src/jmh/java/software/amazon/jdbc/benchmarks/testplugin/TestConnectionWrapper.java @@ -20,7 +20,7 @@ import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.ConnectionPluginManager; -import software.amazon.jdbc.HostListProviderService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.PluginManagerService; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.wrapper.ConnectionWrapper; diff --git a/wrapper/src/main/java/software/amazon/jdbc/BlockingHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/BlockingHostListProvider.java index 9fe7e40fb..31f3d1182 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/BlockingHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/BlockingHostListProvider.java @@ -19,6 +19,7 @@ import java.sql.SQLException; import java.util.List; import java.util.concurrent.TimeoutException; +import software.amazon.jdbc.hostlistprovider.HostListProvider; public interface BlockingHostListProvider extends HostListProvider { diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPlugin.java index d2d72b05c..ad271f2ad 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPlugin.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; /** * Interface for connection plugins. This class implements ways to execute a JDBC method and to clean up resources used diff --git a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java index 2697c5b03..33f7618b9 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/ConnectionPluginManager.java @@ -30,6 +30,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.cleanup.CanReleaseResources; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.AuroraConnectionTrackerPlugin; import software.amazon.jdbc.plugin.AuroraInitialConnectionStrategyPlugin; import software.amazon.jdbc.plugin.AwsSecretsManagerConnectionPlugin; diff --git a/wrapper/src/main/java/software/amazon/jdbc/PartialPluginService.java b/wrapper/src/main/java/software/amazon/jdbc/PartialPluginService.java index 50a1e3667..078626d5b 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PartialPluginService.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PartialPluginService.java @@ -41,11 +41,14 @@ import software.amazon.jdbc.exceptions.ExceptionManager; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.HostAvailabilityStrategyFactory; +import software.amazon.jdbc.hostlistprovider.HostListProvider; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.hostlistprovider.StaticHostListProvider; import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.states.SessionStateService; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.Utils; import software.amazon.jdbc.util.storage.CacheMap; @@ -130,7 +133,7 @@ public PartialPluginService( ? this.configurationProfile.getExceptionHandler() : null; - HostListProviderSupplier supplier = this.dbDialect.getHostListProvider(); + HostListProviderSupplier supplier = this.dbDialect.getHostListProviderSupplier(); this.hostListProvider = supplier.getProvider(this.props, this.originalUrl, this.servicesContainer); } @@ -157,7 +160,7 @@ public HostSpec getCurrentHostSpec() { Messages.get("PluginServiceImpl.currentHostNotAllowed", new Object[] { currentHostSpec == null ? "" : currentHostSpec.getHostAndPort(), - Utils.logTopology(allowedHosts, "")}) + LogUtils.logTopology(allowedHosts, "")}) ); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/PluginService.java b/wrapper/src/main/java/software/amazon/jdbc/PluginService.java index aae57d7f4..bc1e232f2 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PluginService.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PluginService.java @@ -28,6 +28,7 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.exceptions.ExceptionHandler; import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.states.SessionStateService; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.telemetry.TelemetryFactory; diff --git a/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java index 4518eff66..025818cfd 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/PluginServiceImpl.java @@ -46,12 +46,15 @@ import software.amazon.jdbc.exceptions.ExceptionManager; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.HostAvailabilityStrategyFactory; +import software.amazon.jdbc.hostlistprovider.HostListProvider; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.hostlistprovider.StaticHostListProvider; import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.states.SessionStateService; import software.amazon.jdbc.states.SessionStateServiceImpl; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.Utils; import software.amazon.jdbc.util.storage.CacheMap; @@ -191,7 +194,7 @@ public HostSpec getCurrentHostSpec() { Messages.get("PluginServiceImpl.currentHostNotAllowed", new Object[] { currentHostSpec == null ? "" : currentHostSpec.getHostAndPort(), - Utils.logTopology(allowedHosts, "")}) + LogUtils.logTopology(allowedHosts, "")}) ); } @@ -707,7 +710,7 @@ public void updateDialect(final @NonNull Connection connection) throws SQLExcept return; } - final HostListProviderSupplier supplier = this.dialect.getHostListProvider(); + final HostListProviderSupplier supplier = this.dialect.getHostListProviderSupplier(); this.setHostListProvider(supplier.getProvider(this.props, this.originalUrl, this.servicesContainer)); this.refreshHostList(connection); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java index 1e44afdb0..8ae253cbd 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraMysqlDialect.java @@ -17,119 +17,85 @@ package software.amazon.jdbc.dialect; import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.util.Arrays; import java.util.Collections; import java.util.List; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.AuroraTopologyUtils; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsHostListProvider; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; -public class AuroraMysqlDialect extends MysqlDialect implements BlueGreenDialect { +public class AuroraMysqlDialect extends MysqlDialect implements TopologyDialect, BlueGreenDialect { - protected final String topologyQuery = + protected static final String AURORA_VERSION_EXISTS_QUERY = "SHOW VARIABLES LIKE 'aurora_version'"; + protected static final String TOPOLOGY_QUERY = "SELECT SERVER_ID, CASE WHEN SESSION_ID = 'MASTER_SESSION_ID' THEN TRUE ELSE FALSE END, " + "CPU, REPLICA_LAG_IN_MILLISECONDS, LAST_UPDATE_TIMESTAMP " + "FROM information_schema.replica_host_status " - // filter out nodes that haven't been updated in the last 5 minutes + // filter out instances that have not been updated in the last 5 minutes + "WHERE time_to_sec(timediff(now(), LAST_UPDATE_TIMESTAMP)) <= 300 OR SESSION_ID = 'MASTER_SESSION_ID' "; - protected final String isWriterQuery = + protected static final String INSTANCE_ID_QUERY = "SELECT @@aurora_server_id, @@aurora_server_id"; + protected static final String WRITER_ID_QUERY = "SELECT SERVER_ID FROM information_schema.replica_host_status " - + "WHERE SESSION_ID = 'MASTER_SESSION_ID' AND SERVER_ID = @@aurora_server_id"; + + "WHERE SESSION_ID = 'MASTER_SESSION_ID' AND SERVER_ID = @@aurora_server_id"; + protected static final String IS_READER_QUERY = "SELECT @@innodb_read_only"; - protected final String nodeIdQuery = "SELECT @@aurora_server_id, @@aurora_server_id"; - protected final String isReaderQuery = "SELECT @@innodb_read_only"; - - private static final String BG_STATUS_QUERY = - "SELECT * FROM mysql.rds_topology"; - - private static final String TOPOLOGY_TABLE_EXIST_QUERY = + protected static final String BG_TOPOLOGY_EXISTS_QUERY = "SELECT 1 AS tmp FROM information_schema.tables WHERE" + " table_schema = 'mysql' AND table_name = 'rds_topology'"; + protected static final String BG_STATUS_QUERY = "SELECT * FROM mysql.rds_topology"; @Override public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery("SHOW VARIABLES LIKE 'aurora_version'"); - if (rs.next()) { - // If variable with such name is presented then it means it's an Aurora cluster - return true; - } - } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } - } - return false; + return dialectUtils.checkExistenceQueries(connection, AURORA_VERSION_EXISTS_QUERY); } @Override public List getDialectUpdateCandidates() { - return Arrays.asList( - DialectCodes.GLOBAL_AURORA_MYSQL, - DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER); + return Collections.singletonList(DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER); } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final TopologyUtils topologyUtils = new AuroraTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new MonitoringRdsHostListProvider( - properties, - initialUrl, - servicesContainer, - this.topologyQuery, - this.nodeIdQuery, - this.isReaderQuery, - this.isWriterQuery); + return new MonitoringRdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } - return new AuroraHostListProvider( - properties, - initialUrl, - servicesContainer, - this.topologyQuery, - this.nodeIdQuery, - this.isReaderQuery); + return new RdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } @Override - public String getBlueGreenStatusQuery() { - return BG_STATUS_QUERY; + public String getTopologyQuery() { + return TOPOLOGY_QUERY; + } + + @Override + public String getInstanceIdQuery() { + return INSTANCE_ID_QUERY; + } + + @Override + public String getWriterIdQuery() { + return WRITER_ID_QUERY; + } + + @Override + public String getIsReaderQuery() { + return IS_READER_QUERY; } @Override public boolean isBlueGreenStatusAvailable(final Connection connection) { - try { - try (Statement statement = connection.createStatement(); - ResultSet rs = statement.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY)) { - return rs.next(); - } - } catch (SQLException ex) { - return false; - } + return dialectUtils.checkExistenceQueries(connection, BG_TOPOLOGY_EXISTS_QUERY); } + @Override + public String getBlueGreenStatusQuery() { + return BG_STATUS_QUERY; + } } - diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java index c88f595af..e43f30cb8 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/AuroraPgDialect.java @@ -24,59 +24,49 @@ import java.util.List; import java.util.logging.Logger; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.AuroraTopologyUtils; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsHostListProvider; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; import software.amazon.jdbc.util.DriverInfo; +import software.amazon.jdbc.util.Messages; -/** - * Suitable for the following AWS PG configurations. - * - Regional Cluster - */ -public class AuroraPgDialect extends PgDialect implements AuroraLimitlessDialect, BlueGreenDialect { - private static final Logger LOGGER = Logger.getLogger(AuroraPgDialect.class.getName()); +public class AuroraPgDialect extends PgDialect implements TopologyDialect, AuroraLimitlessDialect, BlueGreenDialect { - protected final String extensionsSql = + protected static final String AURORA_UTILS_EXIST_QUERY = "SELECT (setting LIKE '%aurora_stat_utils%') AS aurora_stat_utils " + "FROM pg_catalog.pg_settings " + "WHERE name OPERATOR(pg_catalog.=) 'rds.extensions'"; - - protected final String topologySql = "SELECT 1 FROM pg_catalog.aurora_replica_status() LIMIT 1"; - - protected final String topologyQuery = + protected static final String TOPOLOGY_EXISTS_QUERY = "SELECT 1 FROM pg_catalog.aurora_replica_status() LIMIT 1"; + protected static final String TOPOLOGY_QUERY = "SELECT SERVER_ID, CASE WHEN SESSION_ID OPERATOR(pg_catalog.=) 'MASTER_SESSION_ID' THEN TRUE ELSE FALSE END, " + "CPU, COALESCE(REPLICA_LAG_IN_MSEC, 0), LAST_UPDATE_TIMESTAMP " + "FROM pg_catalog.aurora_replica_status() " - // filter out nodes that haven't been updated in the last 5 minutes + // filter out instances that haven't been updated in the last 5 minutes + "WHERE EXTRACT(" + "EPOCH FROM(pg_catalog.NOW() OPERATOR(pg_catalog.-) LAST_UPDATE_TIMESTAMP)) OPERATOR(pg_catalog.<=) 300 " + "OR SESSION_ID OPERATOR(pg_catalog.=) 'MASTER_SESSION_ID' " + "OR LAST_UPDATE_TIMESTAMP IS NULL"; - protected final String isWriterQuery = + protected static final String INSTANCE_ID_QUERY = + "SELECT pg_catalog.aurora_db_instance_identifier(), pg_catalog.aurora_db_instance_identifier()"; + protected static final String WRITER_ID_QUERY = "SELECT SERVER_ID FROM pg_catalog.aurora_replica_status() " + "WHERE SESSION_ID OPERATOR(pg_catalog.=) 'MASTER_SESSION_ID' " + "AND SERVER_ID OPERATOR(pg_catalog.=) pg_catalog.aurora_db_instance_identifier()"; + protected static final String IS_READER_QUERY = "SELECT pg_catalog.pg_is_in_recovery()"; - protected final String nodeIdQuery = - "SELECT pg_catalog.aurora_db_instance_identifier(), pg_catalog.aurora_db_instance_identifier()"; - protected final String isReaderQuery = "SELECT pg_catalog.pg_is_in_recovery()"; protected static final String LIMITLESS_ROUTER_ENDPOINT_QUERY = "select router_endpoint, load from pg_catalog.aurora_limitless_router_endpoints()"; - private static final String BG_STATUS_QUERY = - "SELECT * FROM " - + "pg_catalog.get_blue_green_fast_switchover_metadata('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; - - private static final String TOPOLOGY_TABLE_EXIST_QUERY = + protected static final String BG_TOPOLOGY_EXISTS_QUERY = "SELECT 'pg_catalog.get_blue_green_fast_switchover_metadata'::regproc"; + protected static final String BG_STATUS_QUERY = + "SELECT * FROM " + + "pg_catalog.get_blue_green_fast_switchover_metadata('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; - @Override - public List getDialectUpdateCandidates() { - return Arrays.asList(DialectCodes.GLOBAL_AURORA_PG, - DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, - DialectCodes.RDS_PG); - } + private static final Logger LOGGER = Logger.getLogger(AuroraPgDialect.class.getName()); @Override public boolean isDialect(final Connection connection) { @@ -84,112 +74,80 @@ public boolean isDialect(final Connection connection) { return false; } - Statement stmt = null; - ResultSet rs = null; boolean hasExtensions = false; - boolean hasTopology = false; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(extensionsSql); - if (rs.next()) { - final boolean auroraUtils = rs.getBoolean("aurora_stat_utils"); - LOGGER.finest(() -> String.format("auroraUtils: %b", auroraUtils)); - if (auroraUtils) { - hasExtensions = true; - } - } - } catch (SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(AURORA_UTILS_EXIST_QUERY)) { + if (!rs.next()) { + return false; } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } + + final boolean auroraUtils = rs.getBoolean("aurora_stat_utils"); + LOGGER.finest(Messages.get("AuroraPgDialect.auroraUtils", new Object[] {auroraUtils})); + if (auroraUtils) { + hasExtensions = true; } + } catch (SQLException ex) { + return false; } + if (!hasExtensions) { return false; } - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(topologySql); - if (rs.next()) { - LOGGER.finest(() -> "hasTopology: true"); - hasTopology = true; - } - } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } - } - return hasExtensions && hasTopology; + + return dialectUtils.checkExistenceQueries(connection, TOPOLOGY_EXISTS_QUERY); + } + + @Override + public List getDialectUpdateCandidates() { + return Arrays.asList(DialectCodes.GLOBAL_AURORA_PG, + DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, + DialectCodes.RDS_PG); } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final TopologyUtils topologyUtils = new AuroraTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new MonitoringRdsHostListProvider( - properties, - initialUrl, - servicesContainer, - topologyQuery, - nodeIdQuery, - isReaderQuery, - isWriterQuery); + return new MonitoringRdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } - return new AuroraHostListProvider( - properties, - initialUrl, - servicesContainer, - topologyQuery, - nodeIdQuery, - isReaderQuery); + return new RdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } + @Override + public String getTopologyQuery() { + return TOPOLOGY_QUERY; + } + + @Override + public String getInstanceIdQuery() { + return INSTANCE_ID_QUERY; + } + + @Override + public String getWriterIdQuery() { + return WRITER_ID_QUERY; + } + + @Override + public String getIsReaderQuery() { + return IS_READER_QUERY; + } + @Override public String getLimitlessRouterEndpointQuery() { return LIMITLESS_ROUTER_ENDPOINT_QUERY; } @Override - public String getBlueGreenStatusQuery() { - return BG_STATUS_QUERY; + public boolean isBlueGreenStatusAvailable(final Connection connection) { + return dialectUtils.checkExistenceQueries(connection, BG_TOPOLOGY_EXISTS_QUERY); } @Override - public boolean isBlueGreenStatusAvailable(final Connection connection) { - try { - try (Statement statement = connection.createStatement(); - ResultSet rs = statement.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY)) { - return rs.next(); - } - } catch (SQLException ex) { - return false; - } + public String getBlueGreenStatusQuery() { + return BG_STATUS_QUERY; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/BlueGreenDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/BlueGreenDialect.java index ce1b678d3..a5e34f150 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/BlueGreenDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/BlueGreenDialect.java @@ -19,7 +19,7 @@ import java.sql.Connection; public interface BlueGreenDialect { - String getBlueGreenStatusQuery(); - boolean isBlueGreenStatusAvailable(final Connection connection); + + String getBlueGreenStatusQuery(); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/Dialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/Dialect.java index 367db7d25..5f09aae0b 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/Dialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/Dialect.java @@ -26,22 +26,23 @@ import software.amazon.jdbc.plugin.failover.FailoverRestriction; public interface Dialect { - int getDefaultPort(); - ExceptionHandler getExceptionHandler(); + boolean isDialect(Connection connection); - String getHostAliasQuery(); + int getDefaultPort(); - String getServerVersionQuery(); + List getDialectUpdateCandidates(); - boolean isDialect(Connection connection); + ExceptionHandler getExceptionHandler(); - List getDialectUpdateCandidates(); + HostListProviderSupplier getHostListProviderSupplier(); - HostListProviderSupplier getHostListProvider(); + String getHostAliasQuery(); void prepareConnectProperties( final @NonNull Properties connectProperties, final @NonNull String protocol, final @NonNull HostSpec hostSpec); EnumSet getFailoverRestrictions(); + + String getServerVersionQuery(); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java index 273edf82a..ed7f4e71e 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectManager.java @@ -57,9 +57,9 @@ public class DialectManager implements DialectProvider { put(DialectCodes.PG, new PgDialect()); put(DialectCodes.MARIADB, new MariaDbDialect()); put(DialectCodes.RDS_MYSQL, new RdsMysqlDialect()); - put(DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER, new RdsMultiAzDbClusterMysqlDialect()); + put(DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER, new MultiAzClusterMysqlDialect()); put(DialectCodes.RDS_PG, new RdsPgDialect()); - put(DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, new RdsMultiAzDbClusterPgDialect()); + put(DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, new MultiAzClusterPgDialect()); put(DialectCodes.GLOBAL_AURORA_MYSQL, new GlobalAuroraMysqlDialect()); put(DialectCodes.AURORA_MYSQL, new AuroraMysqlDialect()); put(DialectCodes.GLOBAL_AURORA_PG, new GlobalAuroraPgDialect()); @@ -75,7 +75,7 @@ public class DialectManager implements DialectProvider { */ protected static final long ENDPOINT_CACHE_EXPIRATION = TimeUnit.HOURS.toNanos(24); - // Map of host name, or url, by dialect code. + // Keys are host names or URLs, values are dialect codes. protected static final CacheMap knownEndpointDialects = new CacheMap<>(); private final RdsUtils rdsHelper = new RdsUtils(); @@ -129,8 +129,7 @@ public Dialect getDialect( this.logCurrentDialect(); return userDialect; } else { - throw new SQLException( - Messages.get("DialectManager.unknownDialectCode", new Object[] {dialectCode})); + throw new SQLException(Messages.get("DialectManager.unknownDialectCode", new Object[] {dialectCode})); } } @@ -140,7 +139,7 @@ public Dialect getDialect( String host = url; final List hosts = this.connectionUrlParser.getHostsFromConnectionUrl( - url, true, pluginService::getHostSpecBuilder); + url, true, pluginService::getHostSpecBuilder); if (!Utils.isNullOrEmpty(hosts)) { host = hosts.get(0).getHost(); } @@ -238,9 +237,10 @@ public Dialect getDialect( for (String dialectCandidateCode : dialectCandidates) { Dialect dialectCandidate = knownDialectsByCode.get(dialectCandidateCode); if (dialectCandidate == null) { - throw new SQLException( - Messages.get("DialectManager.unknownDialectCode", new Object[] {dialectCandidateCode})); + throw new SQLException(Messages.get( + "DialectManager.unknownDialectCode", new Object[] {dialectCandidateCode})); } + boolean isDialect = dialectCandidate.isDialect(connection); if (isDialect) { this.canUpdate = false; @@ -270,9 +270,8 @@ public Dialect getDialect( } private void logCurrentDialect() { - LOGGER.finest(() -> String.format("Current dialect: %s, %s, canUpdate: %b", - this.dialectCode, - this.dialect == null ? "" : this.dialect, - this.canUpdate)); + LOGGER.finest(Messages.get( + "DialectManager.currentDialect", + new Object[] {this.dialectCode, this.dialect == null ? "" : this.dialect, this.canUpdate})); } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectUtils.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectUtils.java new file mode 100644 index 000000000..a09480cd7 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/DialectUtils.java @@ -0,0 +1,46 @@ +/* + * 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.dialect; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class DialectUtils { + /** + * Given a series of existence queries, returns true if they all execute successfully and contain at least one record. + * Otherwise, returns false. + * + * @param conn the connection to use for executing the queries. + * @param existenceQueries the queries to check for existing records. + * @return true if all queries execute successfully and return at least one record, false otherwise. + */ + public boolean checkExistenceQueries(Connection conn, String... existenceQueries) { + for (String existenceQuery : existenceQueries) { + try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(existenceQuery)) { + if (!rs.next()) { + return false; + } + } catch (SQLException e) { + return false; + } + } + + return true; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraMysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraMysqlDialect.java index 3e4db74e6..334757af9 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraMysqlDialect.java @@ -23,79 +23,48 @@ import java.util.Collections; import java.util.List; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.hostlistprovider.AuroraGlobalDbHostListProvider; -import software.amazon.jdbc.hostlistprovider.monitoring.AuroraGlobalDbMonitoringHostListProvider; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraTopologyUtils; +import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringGlobalAuroraHostListProvider; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; -public class GlobalAuroraMysqlDialect extends AuroraMysqlDialect { +public class GlobalAuroraMysqlDialect extends AuroraMysqlDialect implements GlobalAuroraTopologyDialect { - protected final String globalDbStatusTableExistQuery = + protected static final String GLOBAL_STATUS_TABLE_EXISTS_QUERY = "SELECT 1 AS tmp FROM information_schema.tables WHERE" + " upper(table_schema) = 'INFORMATION_SCHEMA' AND upper(table_name) = 'AURORA_GLOBAL_DB_STATUS'"; - - protected final String globalDbStatusQuery = - "SELECT count(1) FROM information_schema.aurora_global_db_status"; - - protected final String globalDbInstanceStatusTableExistQuery = + protected static final String GLOBAL_INSTANCE_STATUS_EXISTS_QUERY = "SELECT 1 AS tmp FROM information_schema.tables WHERE" + " upper(table_schema) = 'INFORMATION_SCHEMA' AND upper(table_name) = 'AURORA_GLOBAL_DB_INSTANCE_STATUS'"; - protected final String globalTopologyQuery = + protected static final String GLOBAL_TOPOLOGY_QUERY = "SELECT SERVER_ID, CASE WHEN SESSION_ID = 'MASTER_SESSION_ID' THEN TRUE ELSE FALSE END, " + "VISIBILITY_LAG_IN_MSEC, AWS_REGION " + "FROM information_schema.aurora_global_db_instance_status "; - protected final String regionByNodeIdQuery = + protected static final String REGION_COUNT_QUERY = "SELECT count(1) FROM information_schema.aurora_global_db_status"; + protected static final String REGION_BY_INSTANCE_ID_QUERY = "SELECT AWS_REGION FROM information_schema.aurora_global_db_instance_status WHERE SERVER_ID = ?"; + @Override public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbStatusTableExistQuery); - - if (rs.next()) { - rs.close(); - stmt.close(); - - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbInstanceStatusTableExistQuery); - - if (rs.next()) { - rs.close(); - stmt.close(); - - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbStatusQuery); + if (!dialectUtils.checkExistenceQueries( + connection, GLOBAL_STATUS_TABLE_EXISTS_QUERY, GLOBAL_INSTANCE_STATUS_EXISTS_QUERY)) { + return false; + } - if (rs.next()) { - int awsRegionCount = rs.getInt(1); - return awsRegionCount > 1; - } - } + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(REGION_COUNT_QUERY)) { + if (!rs.next()) { + return false; } - return false; + + int awsRegionCount = rs.getInt(1); + return awsRegionCount > 1; } catch (final SQLException ex) { - // ignore - } finally { - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } + return false; } - return false; } @Override @@ -104,27 +73,25 @@ public boolean isDialect(final Connection connection) { } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final GlobalAuroraTopologyUtils topologyUtils = + new GlobalAuroraTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new AuroraGlobalDbMonitoringHostListProvider( - properties, - initialUrl, - servicesContainer, - this.globalTopologyQuery, - this.nodeIdQuery, - this.isReaderQuery, - this.isWriterQuery, - this.regionByNodeIdQuery); + return new MonitoringGlobalAuroraHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } - return new AuroraGlobalDbHostListProvider( - properties, - initialUrl, - servicesContainer, - this.globalTopologyQuery, - this.nodeIdQuery, - this.isReaderQuery); + return new GlobalAuroraHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } + + @Override + public String getTopologyQuery() { + return GLOBAL_TOPOLOGY_QUERY; + } + + @Override + public String getRegionByInstanceIdQuery() { + return REGION_BY_INSTANCE_ID_QUERY; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraPgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraPgDialect.java index 289ced4ae..7c060c800 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraPgDialect.java @@ -24,91 +24,62 @@ import java.util.List; import java.util.logging.Logger; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.hostlistprovider.AuroraGlobalDbHostListProvider; -import software.amazon.jdbc.hostlistprovider.monitoring.AuroraGlobalDbMonitoringHostListProvider; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraTopologyUtils; +import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringGlobalAuroraHostListProvider; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; +import software.amazon.jdbc.util.Messages; -public class GlobalAuroraPgDialect extends AuroraPgDialect { +public class GlobalAuroraPgDialect extends AuroraPgDialect implements GlobalAuroraTopologyDialect { - private static final Logger LOGGER = Logger.getLogger(GlobalAuroraPgDialect.class.getName()); - - protected final String globalDbStatusFuncExistQuery = - "select 'aurora_global_db_status'::regproc"; - - protected final String globalDbInstanceStatusFuncExistQuery = + protected static final String GLOBAL_STATUS_FUNC_EXISTS_QUERY = "select 'aurora_global_db_status'::regproc"; + protected static final String GLOBAL_INSTANCE_STATUS_FUNC_EXISTS_QUERY = "select 'aurora_global_db_instance_status'::regproc"; - protected final String globalTopologyQuery = + protected static final String GLOBAL_TOPOLOGY_QUERY = "SELECT SERVER_ID, CASE WHEN SESSION_ID = 'MASTER_SESSION_ID' THEN TRUE ELSE FALSE END, " + "VISIBILITY_LAG_IN_MSEC, AWS_REGION " + "FROM aurora_global_db_instance_status()"; - protected final String globalDbStatusQuery = - "SELECT count(1) FROM aurora_global_db_status()"; - - protected final String regionByNodeIdQuery = + protected static final String REGION_COUNT_QUERY = "SELECT count(1) FROM aurora_global_db_status()"; + protected static final String REGION_BY_INSTANCE_ID_QUERY = "SELECT AWS_REGION FROM aurora_global_db_instance_status() WHERE SERVER_ID = ?"; + private static final Logger LOGGER = Logger.getLogger(GlobalAuroraPgDialect.class.getName()); + @Override public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.extensionsSql); - if (rs.next()) { + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(AURORA_UTILS_EXIST_QUERY)) { + if (!rs.next()) { + return false; + } + final boolean auroraUtils = rs.getBoolean("aurora_stat_utils"); - LOGGER.finest(() -> String.format("auroraUtils: %b", auroraUtils)); + LOGGER.finest(Messages.get("AuroraPgDialect.auroraUtils", new Object[] {auroraUtils})); if (!auroraUtils) { return false; } } - rs.close(); - stmt.close(); - - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbStatusFuncExistQuery); - - if (rs.next()) { - rs.close(); - stmt.close(); - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbInstanceStatusFuncExistQuery); - - if (rs.next()) { - rs.close(); - stmt.close(); - - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.globalDbStatusQuery); + if (!dialectUtils.checkExistenceQueries( + connection, GLOBAL_STATUS_FUNC_EXISTS_QUERY, GLOBAL_INSTANCE_STATUS_FUNC_EXISTS_QUERY)) { + return false; + } - if (rs.next()) { - int awsRegionCount = rs.getInt(1); - return awsRegionCount > 1; - } + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(REGION_COUNT_QUERY)) { + if (!rs.next()) { + return false; } + + int awsRegionCount = rs.getInt(1); + return awsRegionCount > 1; } - return false; } catch (final SQLException ex) { - // ignore - } finally { - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } + return false; } - return false; } @Override @@ -117,27 +88,25 @@ public boolean isDialect(final Connection connection) { } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final GlobalAuroraTopologyUtils topologyUtils = + new GlobalAuroraTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new AuroraGlobalDbMonitoringHostListProvider( - properties, - initialUrl, - servicesContainer, - this.globalTopologyQuery, - this.nodeIdQuery, - this.isReaderQuery, - this.isWriterQuery, - this.regionByNodeIdQuery); + return new MonitoringGlobalAuroraHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } - return new AuroraGlobalDbHostListProvider( - properties, - initialUrl, - servicesContainer, - this.globalTopologyQuery, - this.nodeIdQuery, - this.isReaderQuery); + return new GlobalAuroraHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } + + @Override + public String getTopologyQuery() { + return GLOBAL_TOPOLOGY_QUERY; + } + + @Override + public String getRegionByInstanceIdQuery() { + return REGION_BY_INSTANCE_ID_QUERY; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraTopologyDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraTopologyDialect.java new file mode 100644 index 000000000..11db48dff --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/GlobalAuroraTopologyDialect.java @@ -0,0 +1,21 @@ +/* + * 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.dialect; + +public interface GlobalAuroraTopologyDialect extends TopologyDialect { + String getRegionByInstanceIdQuery(); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/HostListProviderSupplier.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/HostListProviderSupplier.java index 0dfe44dc5..bee378f9f 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/HostListProviderSupplier.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/HostListProviderSupplier.java @@ -18,7 +18,7 @@ import java.util.Properties; import org.checkerframework.checker.nullness.qual.NonNull; -import software.amazon.jdbc.HostListProvider; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.util.FullServicesContainer; @FunctionalInterface diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java index 3b368a8a1..58453f6fa 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MariaDbDialect.java @@ -32,45 +32,23 @@ import software.amazon.jdbc.plugin.failover.FailoverRestriction; public class MariaDbDialect implements Dialect { + + protected static final String VERSION_QUERY = "SELECT VERSION()"; + protected static final String HOST_ALIAS_QUERY = "SELECT CONCAT(@@hostname, ':', @@port)"; + + private static MariaDBExceptionHandler mariaDBExceptionHandler; + private static final EnumSet NO_FAILOVER_RESTRICTIONS = + EnumSet.noneOf(FailoverRestriction.class); private static final List dialectUpdateCandidates = Arrays.asList( DialectCodes.AURORA_MYSQL, DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER, DialectCodes.RDS_MYSQL, DialectCodes.MYSQL); - private static MariaDBExceptionHandler mariaDBExceptionHandler; - - private static final EnumSet NO_RESTRICTIONS = EnumSet.noneOf(FailoverRestriction.class); - - @Override - public int getDefaultPort() { - return 3306; - } - - @Override - public ExceptionHandler getExceptionHandler() { - if (mariaDBExceptionHandler == null) { - mariaDBExceptionHandler = new MariaDBExceptionHandler(); - } - return mariaDBExceptionHandler; - } - - @Override - public String getHostAliasQuery() { - return "SELECT CONCAT(@@hostname, ':', @@port)"; - } - - @Override - public String getServerVersionQuery() { - return "SELECT VERSION()"; - } @Override public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.getServerVersionQuery()); + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(VERSION_QUERY)) { while (rs.next()) { final String columnValue = rs.getString(1); if (columnValue != null && columnValue.toLowerCase().contains("mariadb")) { @@ -78,32 +56,31 @@ public boolean isDialect(final Connection connection) { } } } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } + return false; } + return false; } + @Override + public int getDefaultPort() { + return 3306; + } + @Override public List getDialectUpdateCandidates() { return dialectUpdateCandidates; } - public HostListProviderSupplier getHostListProvider() { + @Override + public ExceptionHandler getExceptionHandler() { + if (mariaDBExceptionHandler == null) { + mariaDBExceptionHandler = new MariaDBExceptionHandler(); + } + return mariaDBExceptionHandler; + } + + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> new ConnectionStringHostListProvider(properties, initialUrl, servicesContainer.getHostListProviderService()); } @@ -116,6 +93,16 @@ public void prepareConnectProperties( @Override public EnumSet getFailoverRestrictions() { - return NO_RESTRICTIONS; + return NO_FAILOVER_RESTRICTIONS; + } + + @Override + public String getServerVersionQuery() { + return VERSION_QUERY; + } + + @Override + public String getHostAliasQuery() { + return HOST_ALIAS_QUERY; } } diff --git a/wrapper/src/test/java/integration/container/aurora/TestAuroraHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterDialect.java similarity index 57% rename from wrapper/src/test/java/integration/container/aurora/TestAuroraHostListProvider.java rename to wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterDialect.java index 5303763f7..4dc0a584d 100644 --- a/wrapper/src/test/java/integration/container/aurora/TestAuroraHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterDialect.java @@ -14,16 +14,10 @@ * limitations under the License. */ -package integration.container.aurora; +package software.amazon.jdbc.dialect; -import java.util.Properties; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; -import software.amazon.jdbc.util.FullServicesContainer; +public interface MultiAzClusterDialect extends TopologyDialect { + String getWriterIdQuery(); -public class TestAuroraHostListProvider extends AuroraHostListProvider { - - public TestAuroraHostListProvider( - FullServicesContainer servicesContainer, Properties properties, String originalUrl) { - super(properties, originalUrl, servicesContainer, "", "", ""); - } + String getWriterIdColumnName(); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterMysqlDialect.java similarity index 52% rename from wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java rename to wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterMysqlDialect.java index 9c920c8e6..9fe7d3b03 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterMysqlDialect.java @@ -26,69 +26,56 @@ import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.hostlistprovider.RdsMultiAzDbClusterListProvider; -import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsMultiAzHostListProvider; +import software.amazon.jdbc.hostlistprovider.MultiAzTopologyUtils; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; +import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsHostListProvider; import software.amazon.jdbc.plugin.failover.FailoverRestriction; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; import software.amazon.jdbc.util.DriverInfo; import software.amazon.jdbc.util.RdsUtils; import software.amazon.jdbc.util.StringUtils; -public class RdsMultiAzDbClusterMysqlDialect extends MysqlDialect { +public class MultiAzClusterMysqlDialect extends MysqlDialect implements MultiAzClusterDialect { - private static final String TOPOLOGY_QUERY = "SELECT id, endpoint, port FROM mysql.rds_topology"; - - private static final String TOPOLOGY_TABLE_EXIST_QUERY = + protected static final String REPORT_HOST_EXISTS_QUERY = "SHOW VARIABLES LIKE 'report_host'"; + protected static final String TOPOLOGY_TABLE_EXISTS_QUERY = "SELECT 1 AS tmp FROM information_schema.tables WHERE" - + " table_schema = 'mysql' AND table_name = 'rds_topology'"; - - // For reader nodes, the query returns a writer node ID. For a writer node, the query returns no data. - private static final String FETCH_WRITER_NODE_QUERY = "SHOW REPLICA STATUS"; - - private static final String FETCH_WRITER_NODE_QUERY_COLUMN_NAME = "Source_Server_Id"; + + " table_schema = 'mysql' AND table_name = 'rds_topology'"; + protected static final String TOPOLOGY_QUERY = "SELECT id, endpoint, port FROM mysql.rds_topology"; - // The query return nodeId and nodeName. + // This query returns both instanceId and instanceName. // For example: "1845128080", "test-multiaz-instance-1" - private static final String NODE_ID_QUERY = "SELECT id, SUBSTRING_INDEX(endpoint, '.', 1)" + protected static final String INSTANCE_ID_QUERY = "SELECT id, SUBSTRING_INDEX(endpoint, '.', 1)" + " FROM mysql.rds_topology" + " WHERE id = @@server_id"; - private static final String IS_READER_QUERY = "SELECT @@read_only"; + // For reader instances, this query returns a writer instance ID. For a writer instance, this query returns no data. + protected static final String WRITER_ID_QUERY = "SHOW REPLICA STATUS"; + protected static final String WRITER_ID_QUERY_COLUMN_NAME = "Source_Server_Id"; + protected static final String IS_READER_QUERY = "SELECT @@read_only"; - private static final EnumSet RDS_MULTI_AZ_RESTRICTIONS = + private static final EnumSet FAILOVER_RESTRICTIONS = EnumSet.of(FailoverRestriction.DISABLE_TASK_A, FailoverRestriction.ENABLE_WRITER_IN_TASK_B); protected final RdsUtils rdsUtils = new RdsUtils(); @Override public boolean isDialect(final Connection connection) { - try { - try (Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY)) { - if (!rs.next()) { - return false; - } - } - - try (Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(TOPOLOGY_QUERY)) { - if (!rs.next()) { - return false; - } - } + if (!dialectUtils.checkExistenceQueries(connection, TOPOLOGY_TABLE_EXISTS_QUERY, TOPOLOGY_QUERY)) { + return false; + } - try (Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery("SHOW VARIABLES LIKE 'report_host'")) { - if (!rs.next()) { - return false; - } - final String reportHost = rs.getString(2); // get variable value; expected value is IP address - return !StringUtils.isNullOrEmpty(reportHost); + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(REPORT_HOST_EXISTS_QUERY)) { + if (!rs.next()) { + return false; } + final String reportHost = rs.getString(2); // Expected value is an IP address + return !StringUtils.isNullOrEmpty(reportHost); } catch (final SQLException ex) { - // ignore + return false; } - return false; } @Override @@ -97,31 +84,14 @@ public boolean isDialect(final Connection connection) { } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final TopologyUtils topologyUtils = new MultiAzTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new MonitoringRdsMultiAzHostListProvider( - properties, - initialUrl, - servicesContainer, - TOPOLOGY_QUERY, - NODE_ID_QUERY, - IS_READER_QUERY, - FETCH_WRITER_NODE_QUERY, - FETCH_WRITER_NODE_QUERY_COLUMN_NAME); - - } else { - return new RdsMultiAzDbClusterListProvider( - properties, - initialUrl, - servicesContainer, - TOPOLOGY_QUERY, - NODE_ID_QUERY, - IS_READER_QUERY, - FETCH_WRITER_NODE_QUERY, - FETCH_WRITER_NODE_QUERY_COLUMN_NAME); + return new MonitoringRdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } + return new RdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } @@ -138,6 +108,31 @@ public void prepareConnectProperties( @Override public EnumSet getFailoverRestrictions() { - return RDS_MULTI_AZ_RESTRICTIONS; + return FAILOVER_RESTRICTIONS; + } + + @Override + public String getTopologyQuery() { + return TOPOLOGY_QUERY; + } + + @Override + public String getInstanceIdQuery() { + return INSTANCE_ID_QUERY; + } + + @Override + public String getIsReaderQuery() { + return IS_READER_QUERY; + } + + @Override + public String getWriterIdQuery() { + return WRITER_ID_QUERY; + } + + @Override + public String getWriterIdColumnName() { + return WRITER_ID_QUERY_COLUMN_NAME; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterPgDialect.java similarity index 59% rename from wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java rename to wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterPgDialect.java index 0f6f51301..ba3600710 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMultiAzDbClusterPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MultiAzClusterPgDialect.java @@ -21,60 +21,48 @@ import java.sql.SQLException; import java.sql.Statement; import java.util.List; -import java.util.logging.Logger; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.exceptions.ExceptionHandler; import software.amazon.jdbc.exceptions.MultiAzDbClusterPgExceptionHandler; -import software.amazon.jdbc.hostlistprovider.RdsMultiAzDbClusterListProvider; -import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsMultiAzHostListProvider; +import software.amazon.jdbc.hostlistprovider.MultiAzTopologyUtils; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; +import software.amazon.jdbc.hostlistprovider.monitoring.MonitoringRdsHostListProvider; import software.amazon.jdbc.plugin.failover2.FailoverConnectionPlugin; import software.amazon.jdbc.util.DriverInfo; -public class RdsMultiAzDbClusterPgDialect extends PgDialect { +public class MultiAzClusterPgDialect extends PgDialect implements MultiAzClusterDialect { - private static final Logger LOGGER = Logger.getLogger(RdsMultiAzDbClusterPgDialect.class.getName()); - - private static MultiAzDbClusterPgExceptionHandler exceptionHandler; - - private static final String TOPOLOGY_QUERY = - "SELECT id, endpoint, port FROM rds_tools.show_topology('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; - - // For reader nodes, the query should return a writer node ID. For a writer node, the query should return no data. - private static final String FETCH_WRITER_NODE_QUERY = - "SELECT multi_az_db_cluster_source_dbi_resource_id FROM rds_tools.multi_az_db_cluster_source_dbi_resource_id()" - + " WHERE multi_az_db_cluster_source_dbi_resource_id OPERATOR(pg_catalog.!=)" - + " (SELECT dbi_resource_id FROM rds_tools.dbi_resource_id())"; - - private static final String IS_RDS_CLUSTER_QUERY = + protected static final String IS_RDS_CLUSTER_QUERY = "SELECT multi_az_db_cluster_source_dbi_resource_id FROM rds_tools.multi_az_db_cluster_source_dbi_resource_id()"; + protected static final String TOPOLOGY_QUERY = + "SELECT id, endpoint, port FROM rds_tools.show_topology('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; - private static final String FETCH_WRITER_NODE_QUERY_COLUMN_NAME = "multi_az_db_cluster_source_dbi_resource_id"; - - // The query return nodeId and nodeName. + // This query returns both instanceId and instanceName. // For example: "db-WQFQKBTL2LQUPIEFIFBGENS4ZQ", "test-multiaz-instance-1" - private static final String NODE_ID_QUERY = "SELECT id, SUBSTRING(endpoint FROM 0 FOR POSITION('.' IN endpoint))" + protected static final String INSTANCE_ID_QUERY = + "SELECT id, SUBSTRING(endpoint FROM 0 FOR POSITION('.' IN endpoint))" + " FROM rds_tools.show_topology()" + " WHERE id OPERATOR(pg_catalog.=) rds_tools.dbi_resource_id()"; + // For reader instances, this query should return a writer instance ID. + // For a writer instance, this query should return no data. + protected static final String WRITER_ID_QUERY = + "SELECT multi_az_db_cluster_source_dbi_resource_id FROM rds_tools.multi_az_db_cluster_source_dbi_resource_id()" + + " WHERE multi_az_db_cluster_source_dbi_resource_id OPERATOR(pg_catalog.!=)" + + " (SELECT dbi_resource_id FROM rds_tools.dbi_resource_id())"; + protected static final String WRITER_ID_QUERY_COLUMN_NAME = "multi_az_db_cluster_source_dbi_resource_id"; + protected static final String IS_READER_QUERY = "SELECT pg_catalog.pg_is_in_recovery()"; - private static final String IS_READER_QUERY = "SELECT pg_catalog.pg_is_in_recovery()"; - - @Override - public ExceptionHandler getExceptionHandler() { - if (exceptionHandler == null) { - exceptionHandler = new MultiAzDbClusterPgExceptionHandler(); - } - return exceptionHandler; - } + private static MultiAzDbClusterPgExceptionHandler exceptionHandler; @Override public boolean isDialect(final Connection connection) { try (Statement stmt = connection.createStatement(); - ResultSet rs = stmt.executeQuery(IS_RDS_CLUSTER_QUERY)) { + ResultSet rs = stmt.executeQuery(IS_RDS_CLUSTER_QUERY)) { return rs.next() && rs.getString(1) != null; } catch (final SQLException ex) { - // ignore + return false; } - return false; } @Override @@ -83,32 +71,48 @@ public boolean isDialect(final Connection connection) { } @Override - public HostListProviderSupplier getHostListProvider() { + public ExceptionHandler getExceptionHandler() { + if (exceptionHandler == null) { + exceptionHandler = new MultiAzDbClusterPgExceptionHandler(); + } + return exceptionHandler; + } + + @Override + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> { final PluginService pluginService = servicesContainer.getPluginService(); + final TopologyUtils topologyUtils = new MultiAzTopologyUtils(this, pluginService.getHostSpecBuilder()); if (pluginService.isPluginInUse(FailoverConnectionPlugin.class)) { - return new MonitoringRdsMultiAzHostListProvider( - properties, - initialUrl, - servicesContainer, - TOPOLOGY_QUERY, - NODE_ID_QUERY, - IS_READER_QUERY, - FETCH_WRITER_NODE_QUERY, - FETCH_WRITER_NODE_QUERY_COLUMN_NAME); - - } else { - - return new RdsMultiAzDbClusterListProvider( - properties, - initialUrl, - servicesContainer, - TOPOLOGY_QUERY, - NODE_ID_QUERY, - IS_READER_QUERY, - FETCH_WRITER_NODE_QUERY, - FETCH_WRITER_NODE_QUERY_COLUMN_NAME); + return new MonitoringRdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); } + + return new RdsHostListProvider(topologyUtils, properties, initialUrl, servicesContainer); }; } + + @Override + public String getTopologyQuery() { + return TOPOLOGY_QUERY; + } + + @Override + public String getInstanceIdQuery() { + return INSTANCE_ID_QUERY; + } + + @Override + public String getIsReaderQuery() { + return IS_READER_QUERY; + } + + @Override + public String getWriterIdQuery() { + return WRITER_ID_QUERY; + } + + @Override + public String getWriterIdColumnName() { + return WRITER_ID_QUERY_COLUMN_NAME; + } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java index 7c930bb5e..f24ad1de8 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/MysqlDialect.java @@ -33,46 +33,26 @@ public class MysqlDialect implements Dialect { + protected static final String VERSION_QUERY = "SHOW VARIABLES LIKE 'version_comment'"; + protected static final String HOST_ALIAS_QUERY = "SELECT CONCAT(@@hostname, ':', @@port)"; + + protected static final DialectUtils dialectUtils = new DialectUtils(); + + private static MySQLExceptionHandler mySQLExceptionHandler; + private static final EnumSet NO_FAILOVER_RESTRICTIONS = + EnumSet.noneOf(FailoverRestriction.class); private static final List dialectUpdateCandidates = Arrays.asList( DialectCodes.GLOBAL_AURORA_MYSQL, DialectCodes.AURORA_MYSQL, DialectCodes.RDS_MULTI_AZ_MYSQL_CLUSTER, DialectCodes.RDS_MYSQL ); - private static MySQLExceptionHandler mySQLExceptionHandler; - - private static final EnumSet NO_RESTRICTIONS = EnumSet.noneOf(FailoverRestriction.class); - - @Override - public int getDefaultPort() { - return 3306; - } - - @Override - public ExceptionHandler getExceptionHandler() { - if (mySQLExceptionHandler == null) { - mySQLExceptionHandler = new MySQLExceptionHandler(); - } - return mySQLExceptionHandler; - } - @Override - public String getHostAliasQuery() { - return "SELECT CONCAT(@@hostname, ':', @@port)"; - } - - @Override - public String getServerVersionQuery() { - return "SHOW VARIABLES LIKE 'version_comment'"; - } @Override public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.getServerVersionQuery()); + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(VERSION_QUERY)) { while (rs.next()) { final String columnValue = rs.getString(2); if (columnValue != null && columnValue.toLowerCase().contains("mysql")) { @@ -80,32 +60,31 @@ public boolean isDialect(final Connection connection) { } } } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } + return false; } + return false; } + @Override + public int getDefaultPort() { + return 3306; + } + @Override public List getDialectUpdateCandidates() { return dialectUpdateCandidates; } - public HostListProviderSupplier getHostListProvider() { + @Override + public ExceptionHandler getExceptionHandler() { + if (mySQLExceptionHandler == null) { + mySQLExceptionHandler = new MySQLExceptionHandler(); + } + return mySQLExceptionHandler; + } + + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> new ConnectionStringHostListProvider(properties, initialUrl, servicesContainer.getHostListProviderService()); } @@ -118,6 +97,16 @@ public void prepareConnectProperties( @Override public EnumSet getFailoverRestrictions() { - return NO_RESTRICTIONS; + return NO_FAILOVER_RESTRICTIONS; + } + + @Override + public String getServerVersionQuery() { + return VERSION_QUERY; + } + + @Override + public String getHostAliasQuery() { + return HOST_ALIAS_QUERY; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java index 40363b4da..13031f012 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/PgDialect.java @@ -36,21 +36,37 @@ */ public class PgDialect implements Dialect { + protected static final String PG_PROC_EXISTS_QUERY = "SELECT 1 FROM pg_catalog.pg_proc LIMIT 1"; + protected static final String VERSION_QUERY = "SELECT 'version', pg_catalog.VERSION()"; + protected static final String HOST_ALIAS_QUERY = + "SELECT pg_catalog.CONCAT(pg_catalog.inet_server_addr(), ':', pg_catalog.inet_server_port())"; + + protected static final DialectUtils dialectUtils = new DialectUtils(); + + private static PgExceptionHandler pgExceptionHandler; + private static final EnumSet NO_FAILOVER_RESTRICTIONS = + EnumSet.noneOf(FailoverRestriction.class); private static final List dialectUpdateCandidates = Arrays.asList( DialectCodes.GLOBAL_AURORA_PG, DialectCodes.AURORA_PG, DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, DialectCodes.RDS_PG); - private static PgExceptionHandler pgExceptionHandler; - - private static final EnumSet NO_RESTRICTIONS = EnumSet.noneOf(FailoverRestriction.class); + @Override + public boolean isDialect(final Connection connection) { + return dialectUtils.checkExistenceQueries(connection, PG_PROC_EXISTS_QUERY); + } @Override public int getDefaultPort() { return 5432; } + @Override + public List getDialectUpdateCandidates() { + return dialectUpdateCandidates; + } + @Override public ExceptionHandler getExceptionHandler() { if (pgExceptionHandler == null) { @@ -60,53 +76,7 @@ public ExceptionHandler getExceptionHandler() { } @Override - public String getHostAliasQuery() { - return "SELECT pg_catalog.CONCAT(pg_catalog.inet_server_addr(), ':', pg_catalog.inet_server_port())"; - } - - @Override - public String getServerVersionQuery() { - return "SELECT 'version', pg_catalog.VERSION()"; - } - - @Override - public boolean isDialect(final Connection connection) { - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery("SELECT 1 FROM pg_catalog.pg_proc LIMIT 1"); - if (rs.next()) { - return true; - } - } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } - } - return false; - } - - @Override - public List getDialectUpdateCandidates() { - return dialectUpdateCandidates; - } - - @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> new ConnectionStringHostListProvider(properties, initialUrl, servicesContainer.getHostListProviderService()); } @@ -119,6 +89,16 @@ public void prepareConnectProperties( @Override public EnumSet getFailoverRestrictions() { - return NO_RESTRICTIONS; + return NO_FAILOVER_RESTRICTIONS; + } + + @Override + public String getServerVersionQuery() { + return VERSION_QUERY; + } + + @Override + public String getHostAliasQuery() { + return HOST_ALIAS_QUERY; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java index b91f6a1e3..1e173c772 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsMysqlDialect.java @@ -26,13 +26,13 @@ public class RdsMysqlDialect extends MysqlDialect implements BlueGreenDialect { - private static final String BG_STATUS_QUERY = - "SELECT * FROM mysql.rds_topology"; - - private static final String TOPOLOGY_TABLE_EXIST_QUERY = + protected static final String REPORT_HOST_EXISTS_QUERY = "SHOW VARIABLES LIKE 'report_host'"; + protected static final String TOPOLOGY_TABLE_EXISTS_QUERY = "SELECT 1 AS tmp FROM information_schema.tables WHERE" + " table_schema = 'mysql' AND table_name = 'rds_topology'"; + protected static final String BG_STATUS_QUERY = "SELECT * FROM mysql.rds_topology"; + private static final List dialectUpdateCandidates = Arrays.asList( DialectCodes.AURORA_MYSQL, DialectCodes.GLOBAL_AURORA_MYSQL, @@ -54,50 +54,34 @@ public boolean isDialect(final Connection connection) { // | Variable_name | value | // |-----------------|---------------------| // | version_comment | Source distribution | - // If super.idDialect returns true there is no need to check for RdsMysqlDialect. + // If super.isDialect returns true there is no need to check for RdsMysqlDialect. return false; } - Statement stmt = null; - ResultSet rs = null; - - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(this.getServerVersionQuery()); - if (!rs.next()) { - return false; - } - final String columnValue = rs.getString(2); - if (!"Source distribution".equalsIgnoreCase(columnValue)) { - return false; - } - rs.close(); - rs = stmt.executeQuery("SHOW VARIABLES LIKE 'report_host'"); - if (!rs.next()) { - return false; - } - final String reportHost = rs.getString(2); // get variable value; expected empty value - return StringUtils.isNullOrEmpty(reportHost); + try (Statement stmt = connection.createStatement()) { + try (ResultSet rs = stmt.executeQuery(VERSION_QUERY)) { + if (!rs.next()) { + return false; + } - } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore + final String columnValue = rs.getString(2); + if (!"Source distribution".equalsIgnoreCase(columnValue)) { + return false; } } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore + + try (ResultSet rs = stmt.executeQuery(REPORT_HOST_EXISTS_QUERY)) { + if (!rs.next()) { + return false; } + + final String reportHost = rs.getString(2); // An empty value is expected + return StringUtils.isNullOrEmpty(reportHost); } + + } catch (final SQLException ex) { + return false; } - return false; } @Override @@ -106,19 +90,12 @@ public List getDialectUpdateCandidates() { } @Override - public String getBlueGreenStatusQuery() { - return BG_STATUS_QUERY; + public boolean isBlueGreenStatusAvailable(final Connection connection) { + return dialectUtils.checkExistenceQueries(connection, TOPOLOGY_TABLE_EXISTS_QUERY); } @Override - public boolean isBlueGreenStatusAvailable(final Connection connection) { - try { - try (Statement statement = connection.createStatement(); - ResultSet rs = statement.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY)) { - return rs.next(); - } - } catch (SQLException ex) { - return false; - } + public String getBlueGreenStatusQuery() { + return BG_STATUS_QUERY; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java index 33d2c480a..62a52d019 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/RdsPgDialect.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.logging.Logger; import software.amazon.jdbc.util.DriverInfo; +import software.amazon.jdbc.util.Messages; /** * Suitable for the following AWS PG configurations. @@ -33,61 +34,42 @@ */ public class RdsPgDialect extends PgDialect implements BlueGreenDialect { - private static final Logger LOGGER = Logger.getLogger(RdsPgDialect.class.getName()); - - private static final List dialectUpdateCandidates = Arrays.asList( - DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, - DialectCodes.GLOBAL_AURORA_PG, - DialectCodes.AURORA_PG); - - private static final String extensionsSql = "SELECT (setting LIKE '%rds_tools%') AS rds_tools, " + protected static final String EXTENSIONS_EXIST_SQL = "SELECT (setting LIKE '%rds_tools%') AS rds_tools, " + "(setting LIKE '%aurora_stat_utils%') AS aurora_stat_utils " + "FROM pg_catalog.pg_settings " + "WHERE name OPERATOR(pg_catalog.=) 'rds.extensions'"; + protected static final String TOPOLOGY_TABLE_EXISTS_QUERY = + "SELECT 'rds_tools.show_topology'::regproc"; - private static final String BG_STATUS_QUERY = + protected static final String BG_STATUS_QUERY = "SELECT * FROM rds_tools.show_topology('aws_jdbc_driver-" + DriverInfo.DRIVER_VERSION + "')"; - private static final String TOPOLOGY_TABLE_EXIST_QUERY = - "SELECT 'rds_tools.show_topology'::regproc"; + private static final Logger LOGGER = Logger.getLogger(RdsPgDialect.class.getName()); + private static final List dialectUpdateCandidates = Arrays.asList( + DialectCodes.RDS_MULTI_AZ_PG_CLUSTER, + DialectCodes.GLOBAL_AURORA_PG, + DialectCodes.AURORA_PG); @Override public boolean isDialect(final Connection connection) { if (!super.isDialect(connection)) { return false; } - Statement stmt = null; - ResultSet rs = null; - try { - stmt = connection.createStatement(); - rs = stmt.executeQuery(extensionsSql); + try (Statement stmt = connection.createStatement(); + ResultSet rs = stmt.executeQuery(EXTENSIONS_EXIST_SQL)) { while (rs.next()) { final boolean rdsTools = rs.getBoolean("rds_tools"); final boolean auroraUtils = rs.getBoolean("aurora_stat_utils"); - LOGGER.finest(() -> String.format("rdsTools: %b, auroraUtils: %b", rdsTools, auroraUtils)); + LOGGER.finest(Messages.get("RdsPgDialect.rdsToolsAuroraUtils", new Object[] {rdsTools, auroraUtils})); if (rdsTools && !auroraUtils) { return true; } } } catch (final SQLException ex) { - // ignore - } finally { - if (stmt != null) { - try { - stmt.close(); - } catch (SQLException ex) { - // ignore - } - } - if (rs != null) { - try { - rs.close(); - } catch (SQLException ex) { - // ignore - } - } + return false; } + return false; } @@ -97,19 +79,12 @@ public List getDialectUpdateCandidates() { } @Override - public String getBlueGreenStatusQuery() { - return BG_STATUS_QUERY; + public boolean isBlueGreenStatusAvailable(final Connection connection) { + return dialectUtils.checkExistenceQueries(connection, TOPOLOGY_TABLE_EXISTS_QUERY); } @Override - public boolean isBlueGreenStatusAvailable(final Connection connection) { - try { - try (Statement statement = connection.createStatement(); - ResultSet rs = statement.executeQuery(TOPOLOGY_TABLE_EXIST_QUERY)) { - return rs.next(); - } - } catch (SQLException ex) { - return false; - } + public String getBlueGreenStatusQuery() { + return BG_STATUS_QUERY; } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/TopologyDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/TopologyDialect.java new file mode 100644 index 000000000..e7aa0f4d2 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/TopologyDialect.java @@ -0,0 +1,27 @@ +/* + * 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.dialect; + +public interface TopologyDialect extends Dialect { + String getTopologyQuery(); + + String getInstanceIdQuery(); + + String getWriterIdQuery(); + + String getIsReaderQuery(); +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/dialect/UnknownDialect.java b/wrapper/src/main/java/software/amazon/jdbc/dialect/UnknownDialect.java index f9bd1e4a3..067261242 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/dialect/UnknownDialect.java +++ b/wrapper/src/main/java/software/amazon/jdbc/dialect/UnknownDialect.java @@ -82,7 +82,7 @@ public List getDialectUpdateCandidates() { } @Override - public HostListProviderSupplier getHostListProvider() { + public HostListProviderSupplier getHostListProviderSupplier() { return (properties, initialUrl, servicesContainer) -> new ConnectionStringHostListProvider(properties, initialUrl, servicesContainer.getHostListProviderService()); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraGlobalDbHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraGlobalDbHostListProvider.java deleted file mode 100644 index d85069323..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraGlobalDbHostListProvider.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.hostlistprovider; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.Arrays; -import java.util.Map; -import java.util.Properties; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.HostSpecBuilder; -import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.util.ConnectionUrlParser; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.RdsUtils; -import software.amazon.jdbc.util.StringUtils; - -public class AuroraGlobalDbHostListProvider extends AuroraHostListProvider { - - static final Logger LOGGER = Logger.getLogger(AuroraGlobalDbHostListProvider.class.getName()); - - public static final AwsWrapperProperty GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS = - new AwsWrapperProperty( - "globalClusterInstanceHostPatterns", - null, - "Comma-separated list of the cluster instance DNS patterns that will be used to " - + "build a complete instance endpoints. " - + "A \"?\" character in these patterns should be used as a placeholder for cluster instance names. " - + "This parameter is required for Global Aurora Databases. " - + "Each region in the Global Aurora Database should be specified in the list."); - - protected final RdsUtils rdsUtils = new RdsUtils(); - - protected Map globalClusterInstanceTemplateByAwsRegion; - - static { - PropertyDefinition.registerPluginProperties(AuroraGlobalDbHostListProvider.class); - } - - public AuroraGlobalDbHostListProvider(Properties properties, String originalUrl, - final FullServicesContainer servicesContainer, String topologyQuery, - String nodeIdQuery, String isReaderQuery) { - super(properties, originalUrl, servicesContainer, topologyQuery, nodeIdQuery, isReaderQuery); - } - - @Override - protected void initSettings() throws SQLException { - super.initSettings(); - - String templates = GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.getString(properties); - if (StringUtils.isNullOrEmpty(templates)) { - throw new SQLException("Parameter 'globalClusterInstanceHostPatterns' is required for Aurora Global Database."); - } - - HostSpecBuilder hostSpecBuilder = this.hostListProviderService.getHostSpecBuilder(); - this.globalClusterInstanceTemplateByAwsRegion = Arrays.stream(templates.split(",")) - .map(x -> ConnectionUrlParser.parseHostPortPairWithRegionPrefix(x.trim(), () -> hostSpecBuilder)) - .collect(Collectors.toMap( - k -> k.getValue1(), - v -> { - this.validateHostPatternSetting(v.getValue2().getHost()); - return v.getValue2(); - })); - LOGGER.finest(() -> "Recognized GDB instance template patterns:\n" - + this.globalClusterInstanceTemplateByAwsRegion.entrySet().stream() - .map(x -> String.format("\t[%s] -> %s", x.getKey(), x.getValue().getHostAndPort())) - .collect(Collectors.joining("\n")) - ); - } - - @Override - protected HostSpec createHost(final ResultSet resultSet) throws SQLException { - - // suggestedWriterNodeId is not used for Aurora Global Database clusters. - // Topology query can detect a writer for itself. - - // According to the topology query the result set - // should contain 4 columns: node ID, 1/0 (writer/reader), node lag in time (msec), AWS region. - String hostName = resultSet.getString(1); - final boolean isWriter = resultSet.getBoolean(2); - final float nodeLag = resultSet.getFloat(3); - final String awsRegion = resultSet.getString(4); - - // Calculate weight based on node lag in time and CPU utilization. - final long weight = Math.round(nodeLag) * 100L; - - final HostSpec clusterInstanceTemplateForRegion = this.globalClusterInstanceTemplateByAwsRegion.get(awsRegion); - if (clusterInstanceTemplateForRegion == null) { - throw new SQLException("Can't find cluster template for region " + awsRegion); - } - - return createHost(hostName, isWriter, weight, Timestamp.from(Instant.now()), clusterInstanceTemplateForRegion); - } -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraHostListProvider.java deleted file mode 100644 index fc53f9e1d..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraHostListProvider.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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.hostlistprovider; - - -import java.util.Properties; -import java.util.logging.Logger; -import software.amazon.jdbc.util.FullServicesContainer; - - -public class AuroraHostListProvider extends RdsHostListProvider { - - static final Logger LOGGER = Logger.getLogger(AuroraHostListProvider.class.getName()); - - public AuroraHostListProvider( - final Properties properties, - final String originalUrl, - final FullServicesContainer servicesContainer, - final String topologyQuery, - final String nodeIdQuery, - final String isReaderQuery) { - super(properties, - originalUrl, - servicesContainer, - topologyQuery, - nodeIdQuery, - isReaderQuery); - } -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraTopologyUtils.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraTopologyUtils.java new file mode 100644 index 000000000..f62415ff9 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/AuroraTopologyUtils.java @@ -0,0 +1,95 @@ +/* + * 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.hostlistprovider; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; +import software.amazon.jdbc.dialect.TopologyDialect; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; + +public class AuroraTopologyUtils extends TopologyUtils { + private static final Logger LOGGER = Logger.getLogger(AuroraTopologyUtils.class.getName()); + + public AuroraTopologyUtils(TopologyDialect dialect, HostSpecBuilder hostSpecBuilder) { + super(dialect, hostSpecBuilder); + } + + @Override + protected @Nullable List getHosts( + Connection conn, ResultSet rs, HostSpec initialHostSpec, HostSpec instanceTemplate) throws SQLException { + // Data in the result set is ordered by last update time, so the latest records are last. + // We add hosts to a map to ensure newer records are not overwritten by older ones. + Map hostsMap = new HashMap<>(); + while (rs.next()) { + try { + HostSpec host = createHost(rs, initialHostSpec, instanceTemplate); + hostsMap.put(host.getHost(), host); + } catch (Exception e) { + LOGGER.finest(Messages.get("TopologyUtils.errorProcessingQueryResults", new Object[] {e.getMessage()})); + return null; + } + } + + return new ArrayList<>(hostsMap.values()); + } + + @Override + public boolean isWriterInstance(final Connection connection) throws SQLException { + try (final Statement stmt = connection.createStatement()) { + try (final ResultSet rs = stmt.executeQuery(this.dialect.getWriterIdQuery())) { + if (rs.next()) { + return !StringUtils.isNullOrEmpty(rs.getString(1)); + } + } + } + + return false; + } + + protected HostSpec createHost(ResultSet rs, HostSpec initialHostSpec, HostSpec instanceTemplate) throws SQLException { + // According to the topology query the result set should contain 4 columns: + // instance ID, 1/0 (writer/reader), CPU utilization, instance lag in time. + String hostName = rs.getString(1); + final boolean isWriter = rs.getBoolean(2); + final double cpuUtilization = rs.getDouble(3); + final double instanceLag = rs.getDouble(4); + Timestamp lastUpdateTime; + try { + lastUpdateTime = rs.getTimestamp(5); + } catch (Exception e) { + lastUpdateTime = Timestamp.from(Instant.now()); + } + + // Calculate weight based on instance lag in time and CPU utilization. + final long weight = Math.round(instanceLag) * 100L + Math.round(cpuUtilization); + + return createHost(hostName, hostName, isWriter, weight, lastUpdateTime, initialHostSpec, instanceTemplate); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/ConnectionStringHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/ConnectionStringHostListProvider.java index 80f55bdad..426ea3963 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/ConnectionStringHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/ConnectionStringHostListProvider.java @@ -25,7 +25,6 @@ import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.util.ConnectionUrlParser; @@ -36,7 +35,6 @@ public class ConnectionStringHostListProvider implements StaticHostListProvider private static final Logger LOGGER = Logger.getLogger(ConnectionStringHostListProvider.class.getName()); final List hostList = new ArrayList<>(); - Properties properties; private boolean isInitialized = false; private final boolean isSingleWriterConnectionString; private final ConnectionUrlParser connectionUrlParser; @@ -75,11 +73,12 @@ private void init() throws SQLException { } this.hostList.addAll( this.connectionUrlParser.getHostsFromConnectionUrl(this.initialUrl, this.isSingleWriterConnectionString, - () -> this.hostListProviderService.getHostSpecBuilder())); + this.hostListProviderService::getHostSpecBuilder)); if (this.hostList.isEmpty()) { throw new SQLException(Messages.get("ConnectionStringHostListProvider.parsedListEmpty", new Object[] {this.initialUrl})); } + this.hostListProviderService.setInitialConnectionHostSpec(this.hostList.get(0)); this.isInitialized = true; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/DynamicHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/DynamicHostListProvider.java index 451c047f3..09d321c41 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/DynamicHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/DynamicHostListProvider.java @@ -16,9 +16,7 @@ package software.amazon.jdbc.hostlistprovider; -import software.amazon.jdbc.HostListProvider; - -// A marker interface for providers that can fetch a host list, and it changes depending on database status -// A good example of such provider would be DB cluster provider (Aurora DB clusters, patroni DB clusters, etc.) -// where cluster topology (nodes, their roles, their statuses) changes over time. +// A marker interface for providers that can fetch a host list reflecting the current database topology. +// Examples include providers for Aurora or Multi-AZ clusters, where the cluster topology, status, and instance roles +// change over time. public interface DynamicHostListProvider extends HostListProvider { } diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraHostListProvider.java new file mode 100644 index 000000000..682ebb31f --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraHostListProvider.java @@ -0,0 +1,73 @@ +/* + * 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.hostlistprovider; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; +import software.amazon.jdbc.AwsWrapperProperty; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.RdsUtils; + +public class GlobalAuroraHostListProvider extends RdsHostListProvider { + + public static final AwsWrapperProperty GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS = + new AwsWrapperProperty( + "globalClusterInstanceHostPatterns", + null, + "Comma-separated list of the cluster instance DNS patterns that will be used to " + + "build a complete instance endpoints. " + + "A \"?\" character in these patterns should be used as a placeholder for cluster instance names. " + + "This parameter is required for Global Aurora Databases. " + + "Each region in the Global Aurora Database should be specified in the list."); + + protected final RdsUtils rdsUtils = new RdsUtils(); + protected final GlobalAuroraTopologyUtils topologyUtils; + + protected Map instanceTemplatesByRegion; + + static { + PropertyDefinition.registerPluginProperties(GlobalAuroraHostListProvider.class); + } + + public GlobalAuroraHostListProvider( + GlobalAuroraTopologyUtils topologyUtils, Properties properties, String originalUrl, + FullServicesContainer servicesContainer) { + super(topologyUtils, properties, originalUrl, servicesContainer); + this.topologyUtils = topologyUtils; + } + + @Override + protected void initSettings() throws SQLException { + super.initSettings(); + + String instanceTemplates = GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.getString(properties); + this.instanceTemplatesByRegion = + this.topologyUtils.parseInstanceTemplates(instanceTemplates, this::validateHostPatternSetting); + } + + @Override + protected List queryForTopology(final Connection conn) throws SQLException { + init(); + return this.topologyUtils.queryForTopology(conn, this.initialHostSpec, this.instanceTemplatesByRegion); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraTopologyUtils.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraTopologyUtils.java new file mode 100644 index 000000000..4a5d5eeea --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/GlobalAuroraTopologyUtils.java @@ -0,0 +1,152 @@ +/* + * 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.hostlistprovider; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; +import software.amazon.jdbc.dialect.GlobalAuroraTopologyDialect; +import software.amazon.jdbc.util.ConnectionUrlParser; +import software.amazon.jdbc.util.LogUtils; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.Pair; +import software.amazon.jdbc.util.StringUtils; + +public class GlobalAuroraTopologyUtils extends AuroraTopologyUtils { + private static final Logger LOGGER = Logger.getLogger(GlobalAuroraTopologyUtils.class.getName()); + + protected final GlobalAuroraTopologyDialect dialect; + + public GlobalAuroraTopologyUtils(GlobalAuroraTopologyDialect dialect, HostSpecBuilder hostSpecBuilder) { + super(dialect, hostSpecBuilder); + this.dialect = dialect; + } + + public @Nullable List queryForTopology( + Connection conn, HostSpec initialHostSpec, Map instanceTemplatesByRegion) + throws SQLException { + int originalNetworkTimeout = setNetworkTimeout(conn); + try (final Statement stmt = conn.createStatement(); + final ResultSet rs = stmt.executeQuery(this.dialect.getTopologyQuery())) { + if (rs.getMetaData().getColumnCount() == 0) { + // We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred. + LOGGER.finest(Messages.get("TopologyUtils.unexpectedTopologyQueryColumnCount")); + return null; + } + + return this.verifyWriter(this.getHosts(rs, initialHostSpec, instanceTemplatesByRegion)); + } catch (final SQLSyntaxErrorException e) { + throw new SQLException(Messages.get("TopologyUtils.invalidQuery"), e); + } finally { + if (originalNetworkTimeout == 0 && !conn.isClosed()) { + conn.setNetworkTimeout(networkTimeoutExecutor, originalNetworkTimeout); + } + } + } + + protected @Nullable List getHosts( + ResultSet rs, HostSpec initialHostSpec, Map instanceTemplatesByRegion) throws SQLException { + // Data in the result set is ordered by last update time, so the latest records are last. + // We add hosts to a map to ensure newer records are not overwritten by older ones. + Map hostsMap = new HashMap<>(); + while (rs.next()) { + try { + HostSpec host = createHost(rs, initialHostSpec, instanceTemplatesByRegion); + hostsMap.put(host.getHost(), host); + } catch (Exception e) { + LOGGER.finest(Messages.get("TopologyUtils.errorProcessingQueryResults", new Object[] {e.getMessage()})); + return null; + } + } + + return new ArrayList<>(hostsMap.values()); + } + + protected HostSpec createHost( + ResultSet rs, HostSpec initialHostSpec, Map instanceTemplatesByRegion) + throws SQLException { + // According to the topology query the result set should contain 4 columns: + // instance ID, 1/0 (writer/reader), node lag in time (msec), AWS region. + String hostName = rs.getString(1); + final boolean isWriter = rs.getBoolean(2); + final float nodeLag = rs.getFloat(3); + final String awsRegion = rs.getString(4); + + // Calculate weight based on node lag in time and CPU utilization. + final long weight = Math.round(nodeLag) * 100L; + + final HostSpec instanceTemplate = instanceTemplatesByRegion.get(awsRegion); + if (instanceTemplate == null) { + throw new SQLException(Messages.get( + "GlobalAuroraTopologyMonitor.cannotFindRegionTemplate", new Object[] {awsRegion})); + } + + return createHost( + hostName, hostName, isWriter, weight, Timestamp.from(Instant.now()), initialHostSpec, instanceTemplate); + } + + public @Nullable String getRegion(String instanceId, Connection conn) throws SQLException { + try (final PreparedStatement stmt = conn.prepareStatement(this.dialect.getRegionByInstanceIdQuery())) { + stmt.setString(1, instanceId); + try (final ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + String awsRegion = rs.getString(1); + return StringUtils.isNullOrEmpty(awsRegion) ? null : awsRegion; + } + } + } + + return null; + } + + public Map parseInstanceTemplates(String instanceTemplatesString, Consumer hostValidator) + throws SQLException { + if (StringUtils.isNullOrEmpty(instanceTemplatesString)) { + throw new SQLException(Messages.get("GlobalAuroraTopologyUtils.globalClusterInstanceHostPatternsRequired")); + } + + Map instanceTemplates = Arrays.stream(instanceTemplatesString.split(",")) + .map(x -> ConnectionUrlParser.parseHostPortPairWithRegionPrefix(x.trim(), () -> hostSpecBuilder)) + .collect(Collectors.toMap( + Pair::getValue1, + v -> { + hostValidator.accept(v.getValue2().getHost()); + return v.getValue2(); + })); + LOGGER.finest(Messages.get( + "GlobalAuroraTopologyUtils.detectedGdbPatterns", + new Object[] {LogUtils.toLogString(instanceTemplates)})); + + return instanceTemplates; + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/HostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProvider.java similarity index 89% rename from wrapper/src/main/java/software/amazon/jdbc/HostListProvider.java rename to wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProvider.java index 0aa93714a..206a35415 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProvider.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package software.amazon.jdbc; +package software.amazon.jdbc.hostlistprovider; import java.sql.Connection; import java.sql.SQLException; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostRole; +import software.amazon.jdbc.HostSpec; public interface HostListProvider { @@ -40,6 +43,7 @@ public interface HostListProvider { */ HostRole getHostRole(Connection connection) throws SQLException; + @Nullable HostSpec identifyConnection(Connection connection) throws SQLException; String getClusterId() throws UnsupportedOperationException, SQLException; diff --git a/wrapper/src/main/java/software/amazon/jdbc/HostListProviderService.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProviderService.java similarity index 89% rename from wrapper/src/main/java/software/amazon/jdbc/HostListProviderService.java rename to wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProviderService.java index b2f6b5353..0413cb423 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/HostListProviderService.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/HostListProviderService.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package software.amazon.jdbc; +package software.amazon.jdbc.hostlistprovider; import java.sql.Connection; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.dialect.Dialect; public interface HostListProviderService { diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/MultiAzTopologyUtils.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/MultiAzTopologyUtils.java new file mode 100644 index 000000000..1e1045002 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/MultiAzTopologyUtils.java @@ -0,0 +1,117 @@ +/* + * 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.hostlistprovider; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; +import software.amazon.jdbc.dialect.MultiAzClusterDialect; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; + +public class MultiAzTopologyUtils extends TopologyUtils { + private static final Logger LOGGER = Logger.getLogger(MultiAzTopologyUtils.class.getName()); + + protected final MultiAzClusterDialect dialect; + + public MultiAzTopologyUtils(MultiAzClusterDialect dialect, HostSpecBuilder hostSpecBuilder) { + super(dialect, hostSpecBuilder); + this.dialect = dialect; + } + + @Override + protected @Nullable List getHosts( + Connection conn, ResultSet rs, HostSpec initialHostSpec, HostSpec instanceTemplate) + throws SQLException { + String writerId = this.getWriterId(conn); + + // Data in the result set is ordered by last update time, so the latest records are last. + // We add hosts to a map to ensure newer records are not overwritten by older ones. + Map hostsMap = new HashMap<>(); + while (rs.next()) { + try { + HostSpec host = createHost(rs, initialHostSpec, instanceTemplate, writerId); + hostsMap.put(host.getHost(), host); + } catch (Exception e) { + LOGGER.finest(Messages.get("TopologyUtils.errorProcessingQueryResults", new Object[]{e.getMessage()})); + return null; + } + } + + return new ArrayList<>(hostsMap.values()); + } + + @Override + public boolean isWriterInstance(final Connection connection) throws SQLException { + try (final Statement stmt = connection.createStatement()) { + + try (final ResultSet rs = stmt.executeQuery(this.dialect.getWriterIdQuery())) { + // When connected to a writer, the result is empty, otherwise it contains a single row. + return !rs.next(); + } + } + } + + protected @Nullable String getWriterId(Connection connection) throws SQLException { + try (final Statement stmt = connection.createStatement()) { + try (final ResultSet rs = stmt.executeQuery(this.dialect.getWriterIdQuery())) { + if (rs.next()) { + String writerId = rs.getString(this.dialect.getWriterIdColumnName()); + if (!StringUtils.isNullOrEmpty(writerId)) { + return writerId; + } + } + } + + // The writer ID is only returned when connected to a reader, so if the query does not return a value, it + // means we are connected to a writer + try (final ResultSet rs = stmt.executeQuery(this.dialect.getInstanceIdQuery())) { + if (rs.next()) { + return rs.getString(1); + } + } + } + + return null; + } + + protected HostSpec createHost( + final ResultSet rs, + final HostSpec initialHostSpec, + final HostSpec instanceTemplate, + final @Nullable String writerId) throws SQLException { + + String endpoint = rs.getString("endpoint"); // "instance-name.XYZ.us-west-2.rds.amazonaws.com" + String instanceName = endpoint.substring(0, endpoint.indexOf(".")); // "instance-name" + String hostId = rs.getString("id"); // "1034958454" + final boolean isWriter = hostId.equals(writerId); + + return createHost( + hostId, instanceName, isWriter, 0, Timestamp.from(Instant.now()), initialHostSpec, instanceTemplate); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java index aed5f83d7..4bd523e39 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsHostListProvider.java @@ -17,45 +17,29 @@ package software.amazon.jdbc.hostlistprovider; import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLSyntaxErrorException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; -import java.util.UUID; -import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Logger; -import java.util.stream.Collectors; -import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.util.ConnectionUrlParser; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.Pair; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; -import software.amazon.jdbc.util.StringUtils; -import software.amazon.jdbc.util.SynchronousExecutor; import software.amazon.jdbc.util.Utils; -import software.amazon.jdbc.util.storage.CacheMap; public class RdsHostListProvider implements DynamicHostListProvider { @@ -84,17 +68,17 @@ public class RdsHostListProvider implements DynamicHostListProvider { + "This pattern is required to be specified for IP address or custom domain connections to AWS RDS " + "clusters. Otherwise, if unspecified, the pattern will be automatically created for AWS RDS clusters."); - protected static final Executor networkTimeoutExecutor = new SynchronousExecutor(); protected static final RdsUtils rdsHelper = new RdsUtils(); protected static final ConnectionUrlParser connectionUrlParser = new ConnectionUrlParser(); protected static final int defaultTopologyQueryTimeoutMs = 5000; + protected final ReentrantLock lock = new ReentrantLock(); + protected final Properties properties; + protected final String originalUrl; protected final FullServicesContainer servicesContainer; protected final HostListProviderService hostListProviderService; - protected final String originalUrl; - protected final String topologyQuery; - protected final String nodeIdQuery; - protected final String isReaderQuery; + protected final TopologyUtils topologyUtils; + protected RdsUrlType rdsUrlType; protected long refreshRateNano = CLUSTER_TOPOLOGY_REFRESH_RATE_MS.defaultValue != null ? TimeUnit.MILLISECONDS.toNanos(Long.parseLong(CLUSTER_TOPOLOGY_REFRESH_RATE_MS.defaultValue)) @@ -102,33 +86,25 @@ public class RdsHostListProvider implements DynamicHostListProvider { protected List hostList = new ArrayList<>(); protected List initialHostList = new ArrayList<>(); protected HostSpec initialHostSpec; - - protected final ReentrantLock lock = new ReentrantLock(); protected String clusterId; - protected HostSpec clusterInstanceTemplate; + protected HostSpec instanceTemplate; protected volatile boolean isInitialized = false; - protected Properties properties; - static { PropertyDefinition.registerPluginProperties(RdsHostListProvider.class); } public RdsHostListProvider( + final TopologyUtils topologyUtils, final Properties properties, final String originalUrl, - final FullServicesContainer servicesContainer, - final String topologyQuery, - final String nodeIdQuery, - final String isReaderQuery) { + final FullServicesContainer servicesContainer) { + this.topologyUtils = topologyUtils; this.properties = properties; this.originalUrl = originalUrl; this.servicesContainer = servicesContainer; this.hostListProviderService = servicesContainer.getHostListProviderService(); - this.topologyQuery = topologyQuery; - this.nodeIdQuery = nodeIdQuery; - this.isReaderQuery = isReaderQuery; } protected void init() throws SQLException { @@ -149,14 +125,12 @@ protected void init() throws SQLException { } protected void initSettings() throws SQLException { - - // initial topology is based on connection string + // The initial topology is based on the connection string. this.initialHostList = connectionUrlParser.getHostsFromConnectionUrl(this.originalUrl, false, this.hostListProviderService::getHostSpecBuilder); if (this.initialHostList == null || this.initialHostList.isEmpty()) { - throw new SQLException(Messages.get("RdsHostListProvider.parsedListEmpty", - new Object[] {this.originalUrl})); + throw new SQLException(Messages.get("RdsHostListProvider.parsedListEmpty", new Object[] {this.originalUrl})); } this.initialHostSpec = this.initialHostList.get(0); this.hostListProviderService.setInitialConnectionHostSpec(this.initialHostSpec); @@ -168,10 +142,10 @@ protected void initSettings() throws SQLException { HostSpecBuilder hostSpecBuilder = this.hostListProviderService.getHostSpecBuilder(); String clusterInstancePattern = CLUSTER_INSTANCE_HOST_PATTERN.getString(this.properties); if (clusterInstancePattern != null) { - this.clusterInstanceTemplate = + this.instanceTemplate = ConnectionUrlParser.parseHostPortPair(clusterInstancePattern, () -> hostSpecBuilder); } else { - this.clusterInstanceTemplate = + this.instanceTemplate = hostSpecBuilder .host(rdsHelper.getRdsInstanceHostPattern(this.initialHostSpec.getHost())) .hostId(this.initialHostSpec.getHostId()) @@ -179,8 +153,7 @@ protected void initSettings() throws SQLException { .build(); } - validateHostPatternSetting(this.clusterInstanceTemplate.getHost()); - + validateHostPatternSetting(this.instanceTemplate.getHost()); this.rdsUrlType = rdsHelper.identifyRdsType(this.initialHostSpec.getHost()); } @@ -189,9 +162,9 @@ protected void initSettings() throws SQLException { * cached copy of topology is returned if it's not yet outdated (controlled by {@link * #refreshRateNano}). * - * @param conn A connection to database to fetch the latest topology, if needed. + * @param conn A connection to database to fetch the latest topology, if needed. * @param forceUpdate If true, it forces a service to ignore cached copy of topology and to fetch - * a fresh one. + * a fresh one. * @return a list of hosts that describes cluster topology. A writer is always at position 0. * Returns an empty list if isn't available or is invalid (doesn't contain a writer). * @throws SQLException if errors occurred while retrieving the topology. @@ -200,20 +173,15 @@ protected FetchTopologyResult getTopology(final Connection conn, final boolean f init(); final List storedHosts = this.getStoredTopology(); - if (storedHosts == null || forceUpdate) { - - // need to re-fetch topology - + // We need to re-fetch topology. if (conn == null) { - // can't fetch the latest topology since no connection - // return original hosts parsed from connection string + // We cannot fetch the latest topology since we do not have access to a connection, so we return the original + // hosts parsed from the connection string. return new FetchTopologyResult(false, this.initialHostList); } - // fetch topology from the DB - final List hosts = queryForTopology(conn); - + final List hosts = this.queryForTopology(conn); if (!Utils.isNullOrEmpty(hosts)) { this.servicesContainer.getStorageService().set(this.clusterId, new Topology(hosts)); return new FetchTopologyResult(false, hosts); @@ -223,7 +191,7 @@ protected FetchTopologyResult getTopology(final Connection conn, final boolean f if (storedHosts == null) { return new FetchTopologyResult(false, this.initialHostList); } else { - // use cached data + // Return the cached data. return new FetchTopologyResult(true, storedHosts); } } @@ -236,144 +204,8 @@ protected FetchTopologyResult getTopology(final Connection conn, final boolean f * @throws SQLException if errors occurred while retrieving the topology. */ protected List queryForTopology(final Connection conn) throws SQLException { - int networkTimeout = -1; - try { - networkTimeout = conn.getNetworkTimeout(); - // The topology query is not monitored by the EFM plugin, so it needs a socket timeout - if (networkTimeout == 0) { - conn.setNetworkTimeout(networkTimeoutExecutor, defaultTopologyQueryTimeoutMs); - } - } catch (SQLException e) { - LOGGER.warning(() -> Messages.get("RdsHostListProvider.errorGettingNetworkTimeout", - new Object[] {e.getMessage()})); - } - - try (final Statement stmt = conn.createStatement(); - final ResultSet resultSet = stmt.executeQuery(this.topologyQuery)) { - return processQueryResults(resultSet); - } catch (final SQLSyntaxErrorException e) { - throw new SQLException(Messages.get("RdsHostListProvider.invalidQuery"), e); - } finally { - if (networkTimeout == 0 && !conn.isClosed()) { - conn.setNetworkTimeout(networkTimeoutExecutor, networkTimeout); - } - } - } - - /** - * Form a list of hosts from the results of the topology query. - * - * @param resultSet The results of the topology query - * @return a list of {@link HostSpec} objects representing - * the topology that was returned by the - * topology query. The list will be empty if the topology query returned an invalid topology - * (no writer instance). - */ - private List processQueryResults(final ResultSet resultSet) throws SQLException { - - final HashMap hostMap = new HashMap<>(); - - // Data is result set is ordered by last updated time so the latest records go last. - // When adding hosts to a map, the newer records replace the older ones. - while (resultSet.next()) { - final HostSpec host = createHost(resultSet); - hostMap.put(host.getHost(), host); - } - - final List hosts = new ArrayList<>(); - final List writers = new ArrayList<>(); - - for (final HostSpec host : hostMap.values()) { - if (host.getRole() != HostRole.WRITER) { - hosts.add(host); - } else { - writers.add(host); - } - } - - int writerCount = writers.size(); - - if (writerCount == 0) { - LOGGER.severe( - () -> Messages.get( - "RdsHostListProvider.invalidTopology")); - hosts.clear(); - } else if (writerCount == 1) { - hosts.add(writers.get(0)); - } else { - // Take the latest updated writer node as the current writer. All others will be ignored. - List sortedWriters = writers.stream() - .sorted(Comparator.comparing(HostSpec::getLastUpdateTime, Comparator.nullsLast(Comparator.reverseOrder()))) - .collect(Collectors.toList()); - hosts.add(sortedWriters.get(0)); - } - - return hosts; - } - - /** - * Creates an instance of HostSpec which captures details about a connectable host. - * - * @param resultSet the result set from querying the topology - * @return a {@link HostSpec} instance for a specific instance from the cluster - * @throws SQLException If unable to retrieve the hostName from the result set - */ - protected HostSpec createHost(final ResultSet resultSet) throws SQLException { - // According to the topology query the result set - // should contain 4 columns: node ID, 1/0 (writer/reader), CPU utilization, node lag in time. - String hostName = resultSet.getString(1); - final boolean isWriter = resultSet.getBoolean(2); - final double cpuUtilization = resultSet.getDouble(3); - final double nodeLag = resultSet.getDouble(4); - Timestamp lastUpdateTime; - try { - lastUpdateTime = resultSet.getTimestamp(5); - } catch (Exception e) { - lastUpdateTime = Timestamp.from(Instant.now()); - } - - // Calculate weight based on node lag in time and CPU utilization. - final long weight = Math.round(nodeLag) * 100L + Math.round(cpuUtilization); - - return createHost(hostName, isWriter, weight, lastUpdateTime, this.clusterInstanceTemplate); - } - - protected HostSpec createHost( - String host, - final boolean isWriter, - final long weight, - final Timestamp lastUpdateTime, - final HostSpec clusterInstanceTemplate) { - - host = host == null ? "?" : host; - final String endpoint = getHostEndpoint(host, clusterInstanceTemplate); - final int port = clusterInstanceTemplate.isPortSpecified() - ? clusterInstanceTemplate.getPort() - : this.initialHostSpec.getPort(); - - final HostSpec hostSpec = this.hostListProviderService.getHostSpecBuilder() - .host(endpoint) - .port(port) - .role(isWriter ? HostRole.WRITER : HostRole.READER) - .availability(HostAvailability.AVAILABLE) - .weight(weight) - .lastUpdateTime(lastUpdateTime) - .build(); - hostSpec.addAlias(host); - hostSpec.setHostId(host); - return hostSpec; - } - - /** - * Build a host dns endpoint based on host/node name. - * - * @param nodeName A host name. - * @param clusterInstanceTemplate A cluster instance template - * @return Host dns endpoint - */ - protected String getHostEndpoint(final String nodeName, final HostSpec clusterInstanceTemplate) { - final String host = clusterInstanceTemplate.getHost(); - return host.replace("?", nodeName); + init(); + return this.topologyUtils.queryForTopology(conn, this.initialHostSpec, this.instanceTemplate); } /** @@ -407,7 +239,7 @@ public List refresh(final Connection connection) throws SQLException { : this.hostListProviderService.getCurrentConnection(); final FetchTopologyResult results = getTopology(currentConnection, false); - LOGGER.finest(() -> Utils.logTopology(results.hosts, results.isCachedData ? "[From cache] Topology:" : null)); + LOGGER.finest(() -> LogUtils.logTopology(results.hosts, results.isCachedData ? "[From cache] Topology:" : null)); this.hostList = results.hosts; return Collections.unmodifiableList(hostList); @@ -426,7 +258,7 @@ public List forceRefresh(final Connection connection) throws SQLExcept : this.hostListProviderService.getCurrentConnection(); final FetchTopologyResult results = getTopology(currentConnection, true); - LOGGER.finest(() -> Utils.logTopology(results.hosts)); + LOGGER.finest(() -> LogUtils.logTopology(results.hosts)); this.hostList = results.hosts; return Collections.unmodifiableList(this.hostList); } @@ -438,9 +270,6 @@ public RdsUrlType getRdsUrlType() throws SQLException { protected void validateHostPatternSetting(final String hostPattern) { if (!rdsHelper.isDnsPatternValid(hostPattern)) { - // "Invalid value for the 'clusterInstanceHostPattern' configuration setting - the host - // pattern must contain a '?' - // character as a placeholder for the DB instance identifiers of the instances in the cluster" final String message = Messages.get("RdsHostListProvider.invalidPattern"); LOGGER.severe(message); throw new RuntimeException(message); @@ -448,18 +277,13 @@ protected void validateHostPatternSetting(final String hostPattern) { final RdsUrlType rdsUrlType = rdsHelper.identifyRdsType(hostPattern); if (rdsUrlType == RdsUrlType.RDS_PROXY || rdsUrlType == RdsUrlType.RDS_PROXY_ENDPOINT) { - // "An RDS Proxy url can't be used as the 'clusterInstanceHostPattern' configuration setting." - final String message = - Messages.get("RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRDSProxy"); + final String message = Messages.get("RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRDSProxy"); LOGGER.severe(message); throw new RuntimeException(message); } if (rdsUrlType == RdsUrlType.RDS_CUSTOM_CLUSTER) { - // "An RDS Custom Cluster endpoint can't be used as the 'clusterInstanceHostPattern' - // configuration setting." - final String message = - Messages.get("RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRdsCustom"); + final String message = Messages.get("RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRdsCustom"); LOGGER.severe(message); throw new RuntimeException(message); } @@ -478,64 +302,54 @@ public FetchTopologyResult(final boolean isCachedData, final List host @Override public HostRole getHostRole(Connection conn) throws SQLException { - try (final Statement stmt = conn.createStatement(); - final ResultSet rs = stmt.executeQuery(this.isReaderQuery)) { - if (rs.next()) { - boolean isReader = rs.getBoolean(1); - return isReader ? HostRole.READER : HostRole.WRITER; - } - } catch (SQLException e) { - throw new SQLException(Messages.get("RdsHostListProvider.errorGettingHostRole"), e); - } - - throw new SQLException(Messages.get("RdsHostListProvider.errorGettingHostRole")); + init(); + return this.topologyUtils.getHostRole(conn); } @Override - public HostSpec identifyConnection(Connection connection) throws SQLException { - try (final Statement stmt = connection.createStatement(); - final ResultSet resultSet = stmt.executeQuery(this.nodeIdQuery)) { - if (resultSet.next()) { - final String instanceName = resultSet.getString(1); + public @Nullable HostSpec identifyConnection(Connection connection) throws SQLException { + init(); + try { + Pair instanceIds = this.topologyUtils.getInstanceId(connection); + if (instanceIds == null) { + throw new SQLException(Messages.get("RdsHostListProvider.errorIdentifyConnection")); + } - List topology = this.refresh(connection); + List topology = this.refresh(connection); + boolean isForcedRefresh = false; + if (topology == null) { + topology = this.forceRefresh(connection); + isForcedRefresh = true; + } - boolean isForcedRefresh = false; - if (topology == null) { - topology = this.forceRefresh(connection); - isForcedRefresh = true; - } + if (topology == null) { + return null; + } + String instanceName = instanceIds.getValue2(); + HostSpec foundHost = topology + .stream() + .filter(host -> Objects.equals(instanceName, host.getHostId())) + .findAny() + .orElse(null); + + if (foundHost == null && !isForcedRefresh) { + topology = this.forceRefresh(connection); if (topology == null) { return null; } - HostSpec foundHost = topology + foundHost = topology .stream() .filter(host -> Objects.equals(instanceName, host.getHostId())) .findAny() .orElse(null); - - if (foundHost == null && !isForcedRefresh) { - topology = this.forceRefresh(connection); - if (topology == null) { - return null; - } - - foundHost = topology - .stream() - .filter(host -> Objects.equals(instanceName, host.getHostId())) - .findAny() - .orElse(null); - } - - return foundHost; } + + return foundHost; } catch (final SQLException e) { throw new SQLException(Messages.get("RdsHostListProvider.errorIdentifyConnection"), e); } - - throw new SQLException(Messages.get("RdsHostListProvider.errorIdentifyConnection")); } @Override diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProvider.java deleted file mode 100644 index a63323176..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProvider.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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.hostlistprovider; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLSyntaxErrorException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Properties; -import java.util.logging.Logger; -import software.amazon.jdbc.HostRole; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.hostavailability.HostAvailability; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.Messages; - -public class RdsMultiAzDbClusterListProvider extends RdsHostListProvider { - private final String fetchWriterNodeQuery; - private final String fetchWriterNodeQueryHeader; - static final Logger LOGGER = Logger.getLogger(RdsMultiAzDbClusterListProvider.class.getName()); - - public RdsMultiAzDbClusterListProvider( - final Properties properties, - final String originalUrl, - final FullServicesContainer servicesContainer, - final String topologyQuery, - final String nodeIdQuery, - final String isReaderQuery, - final String fetchWriterNodeQuery, - final String fetchWriterNodeQueryHeader - ) { - super(properties, - originalUrl, - servicesContainer, - topologyQuery, - nodeIdQuery, - isReaderQuery); - this.fetchWriterNodeQuery = fetchWriterNodeQuery; - this.fetchWriterNodeQueryHeader = fetchWriterNodeQueryHeader; - } - - /** - * Obtain a cluster topology from database. - * - * @param conn A connection to database to fetch the latest topology. - * @return a list of {@link HostSpec} objects representing the topology - * @throws SQLException if errors occurred while retrieving the topology. - */ - protected List queryForTopology(final Connection conn) throws SQLException { - int networkTimeout = -1; - try { - networkTimeout = conn.getNetworkTimeout(); - // The topology query is not monitored by the EFM plugin, so it needs a socket timeout - if (networkTimeout == 0) { - conn.setNetworkTimeout(networkTimeoutExecutor, defaultTopologyQueryTimeoutMs); - } - } catch (SQLException e) { - LOGGER.warning(() -> Messages.get("RdsHostListProvider.errorGettingNetworkTimeout", - new Object[] {e.getMessage()})); - } - - try { - final Statement stmt = conn.createStatement(); - String writerNodeId = processWriterNodeId(stmt.executeQuery(this.fetchWriterNodeQuery)); - if (writerNodeId == null) { - final ResultSet nodeIdResultSet = stmt.executeQuery(this.nodeIdQuery); - while (nodeIdResultSet.next()) { - writerNodeId = nodeIdResultSet.getString(1); - } - } - final ResultSet topologyResultSet = stmt.executeQuery(this.topologyQuery); - return processTopologyQueryResults(topologyResultSet, writerNodeId); - } catch (final SQLSyntaxErrorException e) { - throw new SQLException(Messages.get("RdsHostListProvider.invalidQuery"), e); - } finally { - if (networkTimeout == 0 && !conn.isClosed()) { - conn.setNetworkTimeout(networkTimeoutExecutor, networkTimeout); - } - } - } - - /** - * Get writer node ID. - * - * @param fetchWriterNodeResultSet A ResultSet of writer node query - * @return String The ID of a writer node - * @throws SQLException if errors occurred while retrieving the topology - */ - private String processWriterNodeId(final ResultSet fetchWriterNodeResultSet) throws SQLException { - String writerNodeId = null; - if (fetchWriterNodeResultSet.next()) { - writerNodeId = fetchWriterNodeResultSet.getString(fetchWriterNodeQueryHeader); - } - return writerNodeId; - } - - /** - * Form a list of hosts from the results of the topology query. - * - * @param topologyResultSet The results of the topology query - * @param writerNodeId The writer node ID - * @return a list of {@link HostSpec} objects representing - * the topology that was returned by the - * topology query. The list will be empty if the topology query returned an invalid topology - * (no writer instance). - */ - private List processTopologyQueryResults( - final ResultSet topologyResultSet, - final String writerNodeId) throws SQLException { - - final HashMap hostMap = new HashMap<>(); - - // Data is result set is ordered by last updated time so the latest records go last. - // When adding hosts to a map, the newer records replace the older ones. - while (topologyResultSet.next()) { - final HostSpec host = createHost(topologyResultSet, writerNodeId); - hostMap.put(host.getHost(), host); - } - - final List hosts = new ArrayList<>(); - final List writers = new ArrayList<>(); - - for (final HostSpec host : hostMap.values()) { - if (host.getRole() != HostRole.WRITER) { - hosts.add(host); - } else { - writers.add(host); - } - } - - int writerCount = writers.size(); - - if (writerCount == 0) { - LOGGER.severe(() -> Messages.get("RdsHostListProvider.invalidTopology")); - hosts.clear(); - } else { - hosts.add(writers.get(0)); - } - - return hosts; - } - - /** - * Creates an instance of HostSpec which captures details about a connectable host. - * - * @param resultSet the result set from querying the topology - * @return a {@link HostSpec} instance for a specific instance from the cluster - * @throws SQLException If unable to retrieve the hostName from the result set - */ - private HostSpec createHost(final ResultSet resultSet, final String writerNodeId) throws SQLException { - - String hostName = resultSet.getString("endpoint"); // "instance-name.XYZ.us-west-2.rds.amazonaws.com" - String instanceName = hostName.substring(0, hostName.indexOf(".")); // "instance-name" - - // "instance-name.XYZ.us-west-2.rds.amazonaws.com" based on cluster instance template - final String endpoint = getHostEndpoint(instanceName); - - String hostId = resultSet.getString("id"); - int queryPort = resultSet.getInt("port"); - final int port = this.clusterInstanceTemplate.isPortSpecified() - ? this.clusterInstanceTemplate.getPort() - : queryPort; - final boolean isWriter = hostId.equals(writerNodeId); - - final HostSpec hostSpec = this.hostListProviderService.getHostSpecBuilder() - .host(endpoint) - .hostId(hostId) - .port(port) - .role(isWriter ? HostRole.WRITER : HostRole.READER) - .availability(HostAvailability.AVAILABLE) - .weight(0) - .lastUpdateTime(Timestamp.from(Instant.now())) - .build(); - hostSpec.addAlias(hostName); - return hostSpec; - } - - /** - * Build a host dns endpoint based on host/node name. - * - * @param nodeName A host name. - * @return Host dns endpoint - */ - protected String getHostEndpoint(final String nodeName) { - final String host = this.clusterInstanceTemplate.getHost(); - return host.replace("?", nodeName); - } -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/StaticHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/StaticHostListProvider.java index b37eb4cc3..13e646a03 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/StaticHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/StaticHostListProvider.java @@ -16,8 +16,6 @@ package software.amazon.jdbc.hostlistprovider; -import software.amazon.jdbc.HostListProvider; - -// A marker interface for providers that fetch node lists, and it never changes since after. -// An example of such provider is a provider that use connection string as a source. +// A marker interface for providers that fetch host lists that do not change over time. +// An example is a provider that uses a connection string to determine the host list. public interface StaticHostListProvider extends HostListProvider {} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/TopologyUtils.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/TopologyUtils.java new file mode 100644 index 000000000..d24f291b6 --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/TopologyUtils.java @@ -0,0 +1,236 @@ +/* + * 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.hostlistprovider; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLSyntaxErrorException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostRole; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.HostSpecBuilder; +import software.amazon.jdbc.dialect.TopologyDialect; +import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.Pair; +import software.amazon.jdbc.util.SynchronousExecutor; + +/** + * An abstract class defining utility methods that can be used to retrieve and process a variety of database topology + * information. This class can be overridden to define logic specific to various database engine deployments + * (e.g. Aurora, Multi-AZ, Global Aurora etc.). + */ +public abstract class TopologyUtils { + private static final Logger LOGGER = Logger.getLogger(TopologyUtils.class.getName()); + protected static final int DEFAULT_QUERY_TIMEOUT_MS = 1000; + + protected final Executor networkTimeoutExecutor = new SynchronousExecutor(); + protected final TopologyDialect dialect; + protected final HostSpecBuilder hostSpecBuilder; + + public TopologyUtils( + TopologyDialect dialect, + HostSpecBuilder hostSpecBuilder) { + this.dialect = dialect; + this.hostSpecBuilder = hostSpecBuilder; + } + + /** + * Query the database for information for each instance in the database topology. + * + * @param conn the connection to use to query the database. + * @param initialHostSpec the {@link HostSpec} that was used to initially connect. + * @param instanceTemplate the template {@link HostSpec} to use when constructing new {@link HostSpec} objects from + * the data returned by the topology query. + * @return a list of {@link HostSpec} objects representing the results of the topology query. + * @throws SQLException if an error occurs when executing the topology or processing the results. + */ + public @Nullable List queryForTopology(Connection conn, HostSpec initialHostSpec, HostSpec instanceTemplate) + throws SQLException { + int originalNetworkTimeout = setNetworkTimeout(conn); + try (final Statement stmt = conn.createStatement(); + final ResultSet rs = stmt.executeQuery(this.dialect.getTopologyQuery())) { + if (rs.getMetaData().getColumnCount() == 0) { + // We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred. + LOGGER.finest(Messages.get("TopologyUtils.unexpectedTopologyQueryColumnCount")); + return null; + } + + return this.verifyWriter(this.getHosts(conn, rs, initialHostSpec, instanceTemplate)); + } catch (final SQLSyntaxErrorException e) { + throw new SQLException(Messages.get("TopologyUtils.invalidQuery"), e); + } finally { + if (originalNetworkTimeout == 0 && !conn.isClosed()) { + conn.setNetworkTimeout(networkTimeoutExecutor, originalNetworkTimeout); + } + } + } + + protected int setNetworkTimeout(Connection conn) { + int networkTimeout = -1; + try { + networkTimeout = conn.getNetworkTimeout(); + // The topology query is not monitored by the EFM plugin, so it needs a socket timeout. + if (networkTimeout == 0) { + conn.setNetworkTimeout(this.networkTimeoutExecutor, DEFAULT_QUERY_TIMEOUT_MS); + } + } catch (SQLException e) { + LOGGER.warning(() -> Messages.get("TopologyUtils.errorGettingNetworkTimeout", new Object[] {e.getMessage()})); + } + return networkTimeout; + } + + protected abstract @Nullable List getHosts( + Connection conn, ResultSet rs, HostSpec initialHostSpec, HostSpec instanceTemplate) throws SQLException; + + protected @Nullable List verifyWriter(@Nullable List allHosts) { + if (allHosts == null) { + return null; + } + + List hosts = new ArrayList<>(); + List writers = new ArrayList<>(); + for (HostSpec host : allHosts) { + if (HostRole.WRITER == host.getRole()) { + writers.add(host); + } else { + hosts.add(host); + } + } + + int writerCount = writers.size(); + if (writerCount == 0) { + LOGGER.warning(() -> Messages.get("TopologyUtils.invalidTopology")); + return null; + } else if (writerCount == 1) { + hosts.add(writers.get(0)); + } else { + // Assume the latest updated writer instance is the current writer. Other potential writers will be ignored. + List sortedWriters = writers.stream() + .sorted(Comparator.comparing(HostSpec::getLastUpdateTime, Comparator.nullsLast(Comparator.reverseOrder()))) + .collect(Collectors.toList()); + hosts.add(sortedWriters.get(0)); + } + + return hosts; + } + + /** + * Creates a {@link HostSpec} from the given topology information. + * + * @param instanceId the database instance identifier, e.g. "mydb-instance-1". + * @param isWriter true if this is a writer instance, false for reader. + * @param weight the instance weight for load balancing. + * @param lastUpdateTime the timestamp of the last update to this instance's information. + * @param initialHostSpec the original host specification used for connecting. + * @param instanceTemplate the template used to construct the new {@link HostSpec}. + * @return a {@link HostSpec} representing the given information. + */ + public HostSpec createHost( + String instanceId, + String instanceName, + final boolean isWriter, + final long weight, + final Timestamp lastUpdateTime, + final HostSpec initialHostSpec, + final HostSpec instanceTemplate) { + instanceName = instanceName == null ? "?" : instanceName; + final String endpoint = instanceTemplate.getHost().replace("?", instanceName); + final int port = instanceTemplate.isPortSpecified() + ? instanceTemplate.getPort() + : initialHostSpec.getPort(); + + final HostSpec hostSpec = this.hostSpecBuilder + .hostId(instanceId) + .host(endpoint) + .port(port) + .role(isWriter ? HostRole.WRITER : HostRole.READER) + .availability(HostAvailability.AVAILABLE) + .weight(weight) + .lastUpdateTime(lastUpdateTime) + .build(); + hostSpec.addAlias(instanceName); + hostSpec.setHostId(instanceName); + return hostSpec; + } + + /** + * Identifies instances across different database types using instanceId and instanceName values. + * + *

Database types handle these identifiers differently: + * - Aurora: Uses the instance name as both instanceId and instanceName + * Example: "test-instance-1" for both values + * - RDS Cluster: Uses distinct values for instanceId and instanceName + * Example: + * instanceId: "db-WQFQKBTL2LQUPIEFIFBGENS4ZQ" + * instanceName: "test-multiaz-instance-1" + */ + public @Nullable Pair getInstanceId(final Connection connection) { + try { + try (final Statement stmt = connection.createStatement(); + final ResultSet rs = stmt.executeQuery(this.dialect.getInstanceIdQuery())) { + if (rs.next()) { + return Pair.create(rs.getString(1), rs.getString(2)); + } + } + } catch (SQLException ex) { + return null; + } + + return null; + } + + /** + * Evaluate whether the given connection is to a writer instance. + * + * @param connection the connection to evaluate. + * @return true if the connection is to a writer instance, false otherwise. + * @throws SQLException if an exception occurs when querying the database or processing the database response. + */ + public abstract boolean isWriterInstance(Connection connection) throws SQLException; + + /** + * Evaluate the database role of the given connection, either {@link HostRole#WRITER} or {@link HostRole#READER}. + * + * @param conn the connection to evaluate. + * @return the database role of the given connection. + * @throws SQLException if an exception occurs when querying the database or processing the database response. + */ + public HostRole getHostRole(Connection conn) throws SQLException { + try (final Statement stmt = conn.createStatement(); + final ResultSet rs = stmt.executeQuery(this.dialect.getIsReaderQuery())) { + if (rs.next()) { + boolean isReader = rs.getBoolean(1); + return isReader ? HostRole.READER : HostRole.WRITER; + } + } catch (SQLException e) { + throw new SQLException(Messages.get("TopologyUtils.errorGettingHostRole"), e); + } + + throw new SQLException(Messages.get("TopologyUtils.errorGettingHostRole")); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/AuroraGlobalDbMonitoringHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/AuroraGlobalDbMonitoringHostListProvider.java deleted file mode 100644 index 50bb4263d..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/AuroraGlobalDbMonitoringHostListProvider.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.hostlistprovider.monitoring; - -import java.sql.SQLException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import java.util.logging.Logger; -import java.util.stream.Collectors; -import software.amazon.jdbc.HostListProviderService; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.HostSpecBuilder; -import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostlistprovider.AuroraGlobalDbHostListProvider; -import software.amazon.jdbc.util.ConnectionUrlParser; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.Pair; -import software.amazon.jdbc.util.RdsUtils; -import software.amazon.jdbc.util.StringUtils; -import software.amazon.jdbc.util.connection.ConnectionService; - -public class AuroraGlobalDbMonitoringHostListProvider extends MonitoringRdsHostListProvider { - - static final Logger LOGGER = Logger.getLogger(AuroraGlobalDbMonitoringHostListProvider.class.getName()); - - protected Map globalClusterInstanceTemplateByAwsRegion = new HashMap<>(); - - protected final RdsUtils rdsUtils = new RdsUtils(); - - protected String regionByNodeIdQuery; - - static { - // Register property definition in AuroraGlobalDbHostListProvider class. It's not a mistake. - PropertyDefinition.registerPluginProperties(AuroraGlobalDbHostListProvider.class); - } - - public AuroraGlobalDbMonitoringHostListProvider(Properties properties, String originalUrl, - final FullServicesContainer servicesContainer, String globalTopologyQuery, - String nodeIdQuery, String isReaderQuery, String writerTopologyQuery, - String regionByNodeIdQuery) { - - super(properties, originalUrl, servicesContainer, globalTopologyQuery, nodeIdQuery, isReaderQuery, - writerTopologyQuery); - this.regionByNodeIdQuery = regionByNodeIdQuery; - } - - @Override - protected void initSettings() throws SQLException { - super.initSettings(); - - String templates = AuroraGlobalDbHostListProvider.GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.getString(properties); - if (StringUtils.isNullOrEmpty(templates)) { - throw new SQLException("Parameter 'globalClusterInstanceHostPatterns' is required for Aurora Global Database."); - } - - HostSpecBuilder hostSpecBuilder = this.hostListProviderService.getHostSpecBuilder(); - this.globalClusterInstanceTemplateByAwsRegion = Arrays.stream(templates.split(",")) - .map(x -> ConnectionUrlParser.parseHostPortPairWithRegionPrefix(x.trim(), () -> hostSpecBuilder)) - .collect(Collectors.toMap( - Pair::getValue1, - v -> { - this.validateHostPatternSetting(v.getValue2().getHost()); - return v.getValue2(); - })); - LOGGER.finest(() -> "Recognized GDB instance template patterns:\n" - + this.globalClusterInstanceTemplateByAwsRegion.entrySet().stream() - .map(x -> String.format("\t[%s] -> %s", x.getKey(), x.getValue().getHostAndPort())) - .collect(Collectors.joining("\n")) - ); - } - - protected ClusterTopologyMonitor initMonitor() throws SQLException { - return this.servicesContainer.getMonitorService().runIfAbsent( - ClusterTopologyMonitorImpl.class, - this.clusterId, - this.servicesContainer, - this.properties, - (servicesContainer) -> - new GlobalDbClusterTopologyMonitorImpl( - servicesContainer, - this.clusterId, - this.initialHostSpec, - this.properties, - this.clusterInstanceTemplate, - this.refreshRateNano, - this.highRefreshRateNano, - this.topologyQuery, - this.writerTopologyQuery, - this.nodeIdQuery, - this.globalClusterInstanceTemplateByAwsRegion, - this.regionByNodeIdQuery)); - } - -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/ClusterTopologyMonitorImpl.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/ClusterTopologyMonitorImpl.java index 80775f00d..590d6d451 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/ClusterTopologyMonitorImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/ClusterTopologyMonitorImpl.java @@ -17,15 +17,9 @@ package software.amazon.jdbc.hostlistprovider.monitoring; import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLSyntaxErrorException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; import java.util.ArrayList; -import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Properties; import java.util.concurrent.ConcurrentHashMap; @@ -39,22 +33,21 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostlistprovider.Topology; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; import software.amazon.jdbc.util.ExecutorFactory; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.Pair; import software.amazon.jdbc.util.PropertyUtils; import software.amazon.jdbc.util.RdsUtils; import software.amazon.jdbc.util.ServiceUtility; -import software.amazon.jdbc.util.StringUtils; import software.amazon.jdbc.util.SynchronousExecutor; import software.amazon.jdbc.util.Utils; import software.amazon.jdbc.util.monitoring.AbstractMonitor; @@ -67,38 +60,22 @@ public class ClusterTopologyMonitorImpl extends AbstractMonitor implements Clust protected static final Executor networkTimeoutExecutor = new SynchronousExecutor(); protected static final RdsUtils rdsHelper = new RdsUtils(); protected static final long monitorTerminationTimeoutSec = 30; - - protected static final int defaultTopologyQueryTimeoutMs = 1000; protected static final int closeConnectionNetworkTimeoutMs = 500; - protected static final int defaultConnectionTimeoutMs = 5000; protected static final int defaultSocketTimeoutMs = 5000; - // Keep monitoring topology with a high rate for 30s after failover. + // Keep monitoring topology at a high rate for 30s after failover. protected static final long highRefreshPeriodAfterPanicNano = TimeUnit.SECONDS.toNanos(30); protected static final long ignoreTopologyRequestNano = TimeUnit.SECONDS.toNanos(10); - protected final long refreshRateNano; - protected final long highRefreshRateNano; - protected final FullServicesContainer servicesContainer; - protected final Properties properties; - protected Properties monitoringProperties; - protected final HostSpec initialHostSpec; - protected final String topologyQuery; - protected final String nodeIdQuery; - protected final String writerTopologyQuery; - protected final HostSpec clusterInstanceTemplate; - - protected String clusterId; protected final AtomicReference writerHostSpec = new AtomicReference<>(null); protected final AtomicReference monitoringConnection = new AtomicReference<>(null); - protected boolean isVerifiedWriterConnection = false; - protected long highRefreshRateEndTimeNano = 0; + protected final Object topologyUpdated = new Object(); protected final AtomicBoolean requestToUpdateTopology = new AtomicBoolean(false); protected final AtomicLong ignoreNewTopologyRequestsEndTimeNano = new AtomicLong(-1); protected final ConcurrentHashMap submittedNodes = new ConcurrentHashMap<>(); - protected ExecutorService nodeExecutorService = null; + protected final ReentrantLock nodeExecutorLock = new ReentrantLock(); protected final AtomicBoolean nodeThreadsStop = new AtomicBoolean(false); protected final AtomicReference nodeThreadsWriterConnection = new AtomicReference<>(null); @@ -106,34 +83,40 @@ public class ClusterTopologyMonitorImpl extends AbstractMonitor implements Clust protected final AtomicReference nodeThreadsReaderConnection = new AtomicReference<>(null); protected final AtomicReference> nodeThreadsLatestTopology = new AtomicReference<>(null); + protected final long refreshRateNano; + protected final long highRefreshRateNano; + protected final TopologyUtils topologyUtils; + protected final FullServicesContainer servicesContainer; + protected final Properties properties; + protected final Properties monitoringProperties; + protected final HostSpec initialHostSpec; + protected final HostSpec instanceTemplate; + + protected ExecutorService nodeExecutorService = null; + protected boolean isVerifiedWriterConnection = false; + protected long highRefreshRateEndTimeNano = 0; + protected String clusterId; + public ClusterTopologyMonitorImpl( final FullServicesContainer servicesContainer, + final TopologyUtils topologyUtils, final String clusterId, final HostSpec initialHostSpec, final Properties properties, - final HostSpec clusterInstanceTemplate, + final HostSpec instanceTemplate, final long refreshRateNano, - final long highRefreshRateNano, - final String topologyQuery, - final String writerTopologyQuery, - final String nodeIdQuery) { + final long highRefreshRateNano) { super(monitorTerminationTimeoutSec); - this.clusterId = clusterId; this.servicesContainer = servicesContainer; + this.topologyUtils = topologyUtils; + this.clusterId = clusterId; this.initialHostSpec = initialHostSpec; - this.clusterInstanceTemplate = clusterInstanceTemplate; + this.instanceTemplate = instanceTemplate; this.properties = properties; this.refreshRateNano = refreshRateNano; this.highRefreshRateNano = highRefreshRateNano; - this.topologyQuery = topologyQuery; - this.writerTopologyQuery = writerTopologyQuery; - this.nodeIdQuery = nodeIdQuery; - - this.initSettings(); - } - protected void initSettings() { this.monitoringProperties = PropertyUtils.copyProperties(properties); this.properties.stringPropertyNames().stream() .filter(p -> p.startsWith(MONITORING_PROPERTY_PREFIX)) @@ -168,10 +151,11 @@ public List forceRefresh(final boolean shouldVerifyWriter, final long if (this.ignoreNewTopologyRequestsEndTimeNano.get() > 0 && System.nanoTime() < this.ignoreNewTopologyRequestsEndTimeNano.get()) { - // Previous failover has just completed. We can use results of it without triggering a new topology update. + // A previous failover event has completed recently. + // We can use the results of it without triggering a new topology update. List currentHosts = getStoredHosts(); LOGGER.finest( - Utils.logTopology(currentHosts, Messages.get("ClusterTopologyMonitorImpl.ignoringTopologyRequest"))); + LogUtils.logTopology(currentHosts, Messages.get("ClusterTopologyMonitorImpl.ignoringTopologyRequest"))); if (currentHosts != null) { return currentHosts; } @@ -190,13 +174,12 @@ public List forceRefresh(final boolean shouldVerifyWriter, final long @Override public List forceRefresh(@Nullable Connection connection, final long timeoutMs) throws SQLException, TimeoutException { - if (this.isVerifiedWriterConnection) { - // Push monitoring thread to refresh topology with a verified connection + // Get the monitoring thread to refresh the topology using a verified connection. return this.waitTillTopologyGetsUpdated(timeoutMs); } - // Otherwise use provided unverified connection to update topology + // Otherwise, use the provided unverified connection to update the topology. return this.fetchTopologyAndUpdateCache(connection); } @@ -207,12 +190,12 @@ protected List waitTillTopologyGetsUpdated(final long timeoutMs) throw synchronized (this.requestToUpdateTopology) { this.requestToUpdateTopology.set(true); - // Notify monitoring thread (that might be sleeping) that topology should be refreshed immediately. + // Notify the monitoring thread, which may be sleeping, that topology should be refreshed immediately. this.requestToUpdateTopology.notifyAll(); } if (timeoutMs == 0) { - LOGGER.finest(Utils.logTopology(currentHosts, Messages.get("ClusterTopologyMonitorImpl.timeoutSetToZero"))); + LOGGER.finest(LogUtils.logTopology(currentHosts, Messages.get("ClusterTopologyMonitorImpl.timeoutSetToZero"))); return currentHosts; } @@ -235,9 +218,8 @@ protected List waitTillTopologyGetsUpdated(final long timeoutMs) throw } if (System.nanoTime() >= end) { - throw new TimeoutException(Messages.get( - "ClusterTopologyMonitorImpl.topologyNotUpdated", - new Object[]{timeoutMs})); + throw new TimeoutException( + Messages.get("ClusterTopologyMonitorImpl.topologyNotUpdated", new Object[] {timeoutMs})); } return latestHosts; @@ -253,7 +235,7 @@ public void stop() { this.nodeThreadsStop.set(true); this.shutdownNodeExecutorService(); - // It breaks a waiting/sleeping cycles in monitoring thread + // This code interrupts the waiting/sleeping cycle in the monitoring thread. synchronized (this.requestToUpdateTopology) { this.requestToUpdateTopology.set(true); this.requestToUpdateTopology.notifyAll(); @@ -274,7 +256,7 @@ public void monitor() throws Exception { try { LOGGER.finest(() -> Messages.get( "ClusterTopologyMonitorImpl.startMonitoringThread", - new Object[]{this.clusterId, this.initialHostSpec.getHost()})); + new Object[] {this.clusterId, this.initialHostSpec.getHost()})); while (!this.stop.get() && !Thread.currentThread().isInterrupted()) { this.lastActivityTimestampNanos.set(System.nanoTime()); @@ -284,7 +266,7 @@ public void monitor() throws Exception { if (this.submittedNodes.isEmpty()) { LOGGER.finest(Messages.get("ClusterTopologyMonitorImpl.startingNodeMonitoringThreads")); - // start node threads + // Start node monitors. this.nodeThreadsStop.set(false); this.nodeThreadsWriterConnection.set(null); this.nodeThreadsReaderConnection.set(null); @@ -293,7 +275,7 @@ public void monitor() throws Exception { List hosts = getStoredHosts(); if (hosts == null) { - // need any connection to get topology + // Use any available connection to get the topology. hosts = this.openAnyConnectionAndUpdateTopology(); } @@ -324,20 +306,17 @@ public void monitor() throws Exception { throw exceptionList.get(0); } } - // It's not possible to call shutdown() on this.nodeExecutorService since more node may be added later. + // We do not call nodeExecutorService.shutdown() here since more node monitors may be submitted later. } - // otherwise let's try it again the next round - + // We will try again in the next iteration. } else { - // node threads are running - // check if writer is already detected + // The node monitors are running, so we check if the writer has been detected. final Connection writerConnection = this.nodeThreadsWriterConnection.get(); final HostSpec writerConnectionHostSpec = this.nodeThreadsWriterHostSpec.get(); if (writerConnection != null && writerConnectionHostSpec != null) { - LOGGER.finest( - Messages.get( - "ClusterTopologyMonitorImpl.writerPickedUpFromNodeMonitors", - new Object[]{writerConnectionHostSpec})); + LOGGER.finest(Messages.get( + "ClusterTopologyMonitorImpl.writerPickedUpFromNodeMonitors", + new Object[] {writerConnectionHostSpec})); this.closeConnection(this.monitoringConnection.get()); this.monitoringConnection.set(writerConnection); @@ -359,7 +338,7 @@ public void monitor() throws Exception { continue; } else { - // update node threads with new nodes in the topology + // Update node monitors with the new instances in the topology List hosts = this.nodeThreadsLatestTopology.get(); if (hosts != null && !this.nodeThreadsStop.get()) { for (HostSpec hostSpec : hosts) { @@ -383,7 +362,7 @@ public void monitor() throws Exception { throw exceptionList.get(0); } } - // It's not possible to call shutdown() on this.nodeExecutorService since more node may be added later. + // We do not call nodeExecutorService.shutdown() here since more node monitors may be submitted later. } } } @@ -391,8 +370,7 @@ public void monitor() throws Exception { this.delay(true); } else { - // regular mode (not panic mode) - + // We are in regular mode (not panic mode). if (!this.submittedNodes.isEmpty()) { this.shutdownNodeExecutorService(); this.submittedNodes.clear(); @@ -400,8 +378,7 @@ public void monitor() throws Exception { final List hosts = this.fetchTopologyAndUpdateCache(this.monitoringConnection.get()); if (hosts == null) { - // can't get topology - // let's switch to panic mode + // Attempt to fetch topology failed, so we switch to panic mode. Connection conn = this.monitoringConnection.get(); this.monitoringConnection.set(null); this.isVerifiedWriterConnection = false; @@ -413,9 +390,9 @@ public void monitor() throws Exception { this.highRefreshRateEndTimeNano = 0; } - // Do not log topology while in high refresh rate. It's noisy! + // We avoid logging the topology while using the high refresh rate because it is too noisy. if (this.highRefreshRateEndTimeNano == 0) { - LOGGER.finest(Utils.logTopology(getStoredHosts())); + LOGGER.finest(LogUtils.logTopology(getStoredHosts())); } this.delay(false); @@ -430,14 +407,14 @@ public void monitor() throws Exception { } catch (final InterruptedException intEx) { Thread.currentThread().interrupt(); } catch (final Exception ex) { - // this should not be reached; log and exit thread + // This should not be reached. if (LOGGER.isLoggable(Level.FINEST)) { - // We want to print full trace stack of the exception. + // We want to print the full trace stack of the exception. LOGGER.log( Level.FINEST, Messages.get( "ClusterTopologyMonitorImpl.exceptionDuringMonitoringStop", - new Object[]{this.initialHostSpec.getHost()}), + new Object[] {this.initialHostSpec.getHost()}), ex); } @@ -452,7 +429,7 @@ public void monitor() throws Exception { LOGGER.finest(() -> Messages.get( "ClusterTopologyMonitorImpl.stopMonitoringThread", - new Object[]{this.initialHostSpec.getHost()})); + new Object[] {this.initialHostSpec.getHost()})); } } @@ -475,7 +452,7 @@ protected void shutdownNodeExecutorService() { this.nodeExecutorService.shutdownNow(); } } catch (InterruptedException e) { - // do nothing + // Do nothing. } this.nodeExecutorService = null; @@ -512,49 +489,48 @@ protected List openAnyConnectionAndUpdateTopology() { Connection conn; - // open a new connection + // Open a new connection. try { conn = this.servicesContainer.getPluginService().forceConnect(this.initialHostSpec, this.monitoringProperties); } catch (SQLException ex) { - // can't connect return null; } if (this.monitoringConnection.compareAndSet(null, conn)) { LOGGER.finest(() -> Messages.get( "ClusterTopologyMonitorImpl.openedMonitoringConnection", - new Object[]{this.initialHostSpec.getHost()})); + new Object[] {this.initialHostSpec.getHost()})); try { - if (!StringUtils.isNullOrEmpty(this.getWriterNodeId(this.monitoringConnection.get()))) { + if (this.topologyUtils.isWriterInstance(this.monitoringConnection.get())) { this.isVerifiedWriterConnection = true; writerVerifiedByThisThread = true; if (rdsHelper.isRdsInstance(this.initialHostSpec.getHost())) { this.writerHostSpec.set(this.initialHostSpec); - LOGGER.finest( - Messages.get( - "ClusterTopologyMonitorImpl.writerMonitoringConnection", - new Object[]{this.writerHostSpec.get().getHost()})); + LOGGER.finest(Messages.get( + "ClusterTopologyMonitorImpl.writerMonitoringConnection", + new Object[] {this.writerHostSpec.get().getHost()})); } else { - final Pair pair = this.getNodeId(this.monitoringConnection.get()); + final Pair pair = this.topologyUtils.getInstanceId(this.monitoringConnection.get()); if (pair != null) { - this.writerHostSpec.set(this.createHost(pair.getValue1(), pair.getValue2(), true, 0, null, - this.getClusterInstanceTemplate(pair.getValue2(), this.monitoringConnection.get()))); - LOGGER.finest( - Messages.get( - "ClusterTopologyMonitorImpl.writerMonitoringConnection", - new Object[]{this.writerHostSpec.get().getHost()})); + HostSpec instanceTemplate = this.getinstanceTemplate(pair.getValue2(), this.monitoringConnection.get()); + HostSpec writerHost = this.topologyUtils.createHost( + pair.getValue1(), pair.getValue2(), true, 0, null, this.initialHostSpec, instanceTemplate); + this.writerHostSpec.set(writerHost); + LOGGER.finest(Messages.get( + "ClusterTopologyMonitorImpl.writerMonitoringConnection", + new Object[] {this.writerHostSpec.get().getHost()})); } } } } catch (SQLException ex) { - // do nothing + // Do nothing. } } else { - // monitoring connection has already been set by other thread - // close new connection as we don't need it + // The monitoring connection has already been detected by another thread. We close the new connection since it + // is not needed anymore. this.closeConnection(conn); } } @@ -570,8 +546,7 @@ protected List openAnyConnectionAndUpdateTopology() { } if (hosts == null) { - // can't get topology; it might be something's wrong with a connection - // close connection + // Attempt to fetch topology failed. There might be something wrong with the connection, so we close it here. Connection connToClose = this.monitoringConnection.get(); this.monitoringConnection.set(null); this.closeConnection(connToClose); @@ -581,33 +556,8 @@ protected List openAnyConnectionAndUpdateTopology() { return hosts; } - protected HostSpec getClusterInstanceTemplate(String nodeId, Connection connection) { - return this.clusterInstanceTemplate; - } - - /** - * Identifies nodes across different database types using nodeId and nodeName values. - * - *

Database types handle these identifiers differently: - * - Aurora: Uses the instance (node) name as both nodeId and nodeName - * Example: "test-instance-1" for both values - * - RDS Cluster: Uses distinct values for nodeId and nodeName - * Example: - * nodeId: "db-WQFQKBTL2LQUPIEFIFBGENS4ZQ" - * nodeName: "test-multiaz-instance-1" - */ - protected Pair getNodeId(final Connection connection) { - try { - try (final Statement stmt = connection.createStatement(); - final ResultSet resultSet = stmt.executeQuery(this.nodeIdQuery)) { - if (resultSet.next()) { - return Pair.create(resultSet.getString(1), resultSet.getString(2)); - } - } - } catch (SQLException ex) { - // do nothing - } - return null; + protected HostSpec getinstanceTemplate(String nodeId, Connection connection) throws SQLException { + return this.instanceTemplate; } protected void closeConnection(final @Nullable Connection connection) { @@ -616,16 +566,16 @@ protected void closeConnection(final @Nullable Connection connection) { try { connection.setNetworkTimeout(networkTimeoutExecutor, closeConnectionNetworkTimeoutMs); } catch (SQLException ex) { - // do nothing + // Do nothing. } connection.close(); } } catch (final SQLException ex) { - // ignore + // Do nothing. } } - // Sleep that can be easily interrupted + // Sleep method that can be easily interrupted. protected void delay(boolean useHighRefreshRate) throws InterruptedException { if (this.highRefreshRateEndTimeNano > 0 && System.nanoTime() < this.highRefreshRateEndTimeNano) { useHighRefreshRate = true; @@ -655,12 +605,16 @@ protected void delay(boolean useHighRefreshRate) throws InterruptedException { } return hosts; } catch (SQLException ex) { - // do nothing - LOGGER.finest(Messages.get("ClusterTopologyMonitorImpl.errorFetchingTopology", new Object[]{ex})); + LOGGER.finest(Messages.get("ClusterTopologyMonitorImpl.errorFetchingTopology", new Object[] {ex})); } + return null; } + protected List queryForTopology(Connection connection) throws SQLException { + return this.topologyUtils.queryForTopology(connection, this.initialHostSpec, this.instanceTemplate); + } + protected void updateTopologyCache(final @NonNull List hosts) { synchronized (this.requestToUpdateTopology) { this.servicesContainer.getStorageService().set(this.clusterId, new Topology(hosts)); @@ -673,161 +627,6 @@ protected void updateTopologyCache(final @NonNull List hosts) { } } - // Returns a writer node ID if connected to a writer node. Returns null otherwise. - protected String getWriterNodeId(final Connection connection) throws SQLException { - try (final Statement stmt = connection.createStatement()) { - try (final ResultSet resultSet = stmt.executeQuery(this.writerTopologyQuery)) { - if (resultSet.next()) { - return resultSet.getString(1); - } - } - } - return null; - } - - protected @Nullable List queryForTopology(final Connection conn) throws SQLException { - int networkTimeout = -1; - try { - networkTimeout = conn.getNetworkTimeout(); - // The topology query is not monitored by the EFM plugin, so it needs a socket timeout - if (networkTimeout == 0) { - conn.setNetworkTimeout(networkTimeoutExecutor, defaultTopologyQueryTimeoutMs); - } - } catch (SQLException e) { - LOGGER.warning(() -> Messages.get("ClusterTopologyMonitorImpl.errorGettingNetworkTimeout", - new Object[] {e.getMessage()})); - } - - final String suggestedWriterNodeId = this.getSuggestedWriterNodeId(conn); - try (final Statement stmt = conn.createStatement(); - final ResultSet resultSet = stmt.executeQuery(this.topologyQuery)) { - return this.processQueryResults(resultSet, suggestedWriterNodeId); - } catch (final SQLSyntaxErrorException e) { - throw new SQLException(Messages.get("ClusterTopologyMonitorImpl.invalidQuery"), e); - } finally { - if (networkTimeout == 0 && !conn.isClosed()) { - conn.setNetworkTimeout(networkTimeoutExecutor, networkTimeout); - } - } - } - - protected String getSuggestedWriterNodeId(final Connection connection) throws SQLException { - // Aurora topology query can detect a writer for itself, so it doesn't need any suggested writer node ID. - return null; // intentionally null - } - - protected @Nullable List processQueryResults( - final ResultSet resultSet, - final String suggestedWriterNodeId) throws SQLException { - - final HashMap hostMap = new HashMap<>(); - - if (resultSet.getMetaData().getColumnCount() == 0) { - // We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred. - LOGGER.finest(Messages.get("ClusterTopologyMonitorImpl.unexpectedTopologyQueryColumnCount")); - return null; - } - - // Data is result set is ordered by last updated time so the latest records go last. - // When adding hosts to a map, the newer records replace the older ones. - while (resultSet.next()) { - try { - final HostSpec host = createHost(resultSet, suggestedWriterNodeId); - hostMap.put(host.getHost(), host); - } catch (Exception e) { - LOGGER.finest( - Messages.get("ClusterTopologyMonitorImpl.errorProcessingQueryResults", new Object[]{e.getMessage()})); - return null; - } - } - - final List hosts = new ArrayList<>(); - final List writers = new ArrayList<>(); - - for (final HostSpec host : hostMap.values()) { - if (host.getRole() != HostRole.WRITER) { - hosts.add(host); - } else { - writers.add(host); - } - } - - int writerCount = writers.size(); - - if (writerCount == 0) { - LOGGER.warning(() -> Messages.get("ClusterTopologyMonitorImpl.invalidTopology")); - hosts.clear(); - } else if (writerCount == 1) { - hosts.add(writers.get(0)); - } else { - // Take the latest updated writer node as the current writer. All others will be ignored. - List sortedWriters = writers.stream() - .sorted(Comparator.comparing(HostSpec::getLastUpdateTime, Comparator.nullsLast(Comparator.reverseOrder()))) - .collect(Collectors.toList()); - hosts.add(sortedWriters.get(0)); - } - - return hosts; - } - - protected HostSpec createHost( - final ResultSet resultSet, - final String suggestedWriterNodeId) throws SQLException { - - // suggestedWriterNodeId is not used for Aurora clusters. Topology query can detect a writer for itself. - - // According to the topology query the result set - // should contain 4 columns: node ID, 1/0 (writer/reader), CPU utilization, node lag in time. - String hostName = resultSet.getString(1); - final boolean isWriter = resultSet.getBoolean(2); - final float cpuUtilization = resultSet.getFloat(3); - final float nodeLag = resultSet.getFloat(4); - Timestamp lastUpdateTime; - try { - lastUpdateTime = resultSet.getTimestamp(5); - } catch (Exception e) { - lastUpdateTime = Timestamp.from(Instant.now()); - } - - // Calculate weight based on node lag in time and CPU utilization. - final long weight = Math.round(nodeLag) * 100L + Math.round(cpuUtilization); - - return createHost(hostName, hostName, isWriter, weight, lastUpdateTime, this.clusterInstanceTemplate); - } - - protected HostSpec createHost( - String nodeId, - String nodeName, - final boolean isWriter, - final long weight, - final Timestamp lastUpdateTime, - final HostSpec clusterInstanceTemplate) { - - nodeName = nodeName == null ? "?" : nodeName; - final String endpoint = getHostEndpoint(nodeName, clusterInstanceTemplate); - final int port = clusterInstanceTemplate.isPortSpecified() - ? clusterInstanceTemplate.getPort() - : this.initialHostSpec.getPort(); - - final HostSpec hostSpec = this.servicesContainer.getHostListProviderService().getHostSpecBuilder() - .hostId(nodeId) - .host(endpoint) - .port(port) - .role(isWriter ? HostRole.WRITER : HostRole.READER) - .availability(HostAvailability.AVAILABLE) - .weight(weight) - .lastUpdateTime(lastUpdateTime) - .build(); - hostSpec.addAlias(nodeName); - hostSpec.setHostId(nodeName); - return hostSpec; - } - - protected String getHostEndpoint(final String nodeName, final HostSpec clusterInstanceTemplate) { - final String host = clusterInstanceTemplate.getHost(); - return host.replace("?", nodeName); - } - private static class NodeMonitoringWorker implements Runnable { private static final Logger LOGGER = Logger.getLogger(NodeMonitoringWorker.class.getName()); @@ -872,26 +671,24 @@ public void run() { } if (connection != null) { - - String writerId = null; + boolean isWriter = false; try { - writerId = this.monitor.getWriterNodeId(connection); - + isWriter = this.monitor.topologyUtils.isWriterInstance(connection); } catch (SQLSyntaxErrorException ex) { - LOGGER.severe(() -> Messages.get("NodeMonitoringThread.invalidWriterQuery", + LOGGER.severe(() -> Messages.get( + "NodeMonitoringThread.invalidWriterQuery", new Object[] {ex.getMessage()})); throw new RuntimeException(ex); - } catch (SQLException ex) { this.monitor.closeConnection(connection); connection = null; } - if (!StringUtils.isNullOrEmpty(writerId)) { + if (isWriter) { try { if (this.servicesContainer.getPluginService().getHostRole(connection) != HostRole.WRITER) { // The first connection after failover may be stale. - writerId = null; + isWriter = false; } } catch (SQLException e) { // Invalid connection, retry. @@ -899,38 +696,36 @@ public void run() { } } - if (!StringUtils.isNullOrEmpty(writerId)) { - // this prevents closing connection in finally block + if (isWriter) { + // This prevents us from closing the connection in the finally block. if (!this.monitor.nodeThreadsWriterConnection.compareAndSet(null, connection)) { - // writer connection is already setup + // The writer connection is already set up, probably by another node monitor. this.monitor.closeConnection(connection); - } else { - // writer connection is successfully set to writerConnection - LOGGER.fine(Messages.get("NodeMonitoringThread.detectedWriter", new Object[]{writerId})); + // Successfully updated the node monitor writer connection. + LOGGER.fine(Messages.get("NodeMonitoringThread.detectedWriter", new Object[] {hostSpec.getUrl()})); // When nodeThreadsWriterConnection and nodeThreadsWriterHostSpec are both set, the topology monitor may // set ignoreNewTopologyRequestsEndTimeNano, in which case other threads will use the cached topology // for the ignore duration, so we need to update the topology before setting nodeThreadsWriterHostSpec. this.monitor.fetchTopologyAndUpdateCache(connection); this.monitor.nodeThreadsWriterHostSpec.set(hostSpec); this.monitor.nodeThreadsStop.set(true); - LOGGER.fine(Utils.logTopology(this.monitor.getStoredHosts())); + LOGGER.fine(LogUtils.logTopology(this.monitor.getStoredHosts())); } - // Setting the connection to null here prevents the final block - // from closing nodeThreadsWriterConnection. + // We set the connection to null to prevent the finally block from closing nodeThreadsWriterConnection. connection = null; return; - } else if (connection != null) { - // this connection is a reader connection + // This connection is a reader connection. if (this.monitor.nodeThreadsWriterConnection.get() == null) { - // while writer connection isn't yet established this reader connection may update topology + // We can use this reader connection to update the topology while we wait for the writer connection to + // be established. if (updateTopology) { this.readerThreadFetchTopology(connection, this.writerHostSpec); } else if (this.monitor.nodeThreadsReaderConnection.get() == null) { if (this.monitor.nodeThreadsReaderConnection.compareAndSet(null, connection)) { - // let's use this connection to update topology + // Use this connection to update the topology. updateTopology = true; this.readerThreadFetchTopology(connection, this.writerHostSpec); } @@ -945,7 +740,8 @@ public void run() { } finally { this.monitor.closeConnection(connection); final long end = System.nanoTime(); - LOGGER.finest(() -> Messages.get("NodeMonitoringThread.threadCompleted", + LOGGER.finest(() -> Messages.get( + "NodeMonitoringThread.threadCompleted", new Object[] {TimeUnit.NANOSECONDS.toMillis(end - start)})); } } @@ -957,7 +753,8 @@ private void readerThreadFetchTopology(final Connection connection, final @Nulla List hosts; try { - hosts = this.monitor.queryForTopology(connection); + hosts = this.monitor.topologyUtils.queryForTopology( + connection, this.monitor.initialHostSpec, this.monitor.instanceTemplate); if (hosts == null) { return; } @@ -965,12 +762,12 @@ private void readerThreadFetchTopology(final Connection connection, final @Nulla return; } - // share this topology so the main monitoring thread be able to adjust node monitoring threads + // Share this topology so that the main monitoring thread can adjust the node monitoring threads. this.monitor.nodeThreadsLatestTopology.set(hosts); if (this.writerChanged) { this.monitor.updateTopologyCache(hosts); - LOGGER.finest(Utils.logTopology(hosts)); + LOGGER.finest(LogUtils.logTopology(hosts)); return; } @@ -981,16 +778,14 @@ private void readerThreadFetchTopology(final Connection connection, final @Nulla if (latestWriterHostSpec != null && writerHostSpec != null && !latestWriterHostSpec.getHostAndPort().equals(writerHostSpec.getHostAndPort())) { - - // writer node has changed this.writerChanged = true; - - LOGGER.fine(() -> Messages.get("NodeMonitoringThread.writerNodeChanged", + LOGGER.fine(() -> Messages.get( + "NodeMonitoringThread.writerNodeChanged", new Object[] {writerHostSpec.getHost(), latestWriterHostSpec.getHost()})); - // we can update topology cache and notify all waiting threads + // Update the topology cache and notify all waiting threads. this.monitor.updateTopologyCache(hosts); - LOGGER.fine(Utils.logTopology(hosts)); + LOGGER.fine(LogUtils.logTopology(hosts)); } } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalAuroraTopologyMonitor.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalAuroraTopologyMonitor.java new file mode 100644 index 000000000..cf5e20a7d --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalAuroraTopologyMonitor.java @@ -0,0 +1,78 @@ +/* + * 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.hostlistprovider.monitoring; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraTopologyUtils; +import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.StringUtils; + + +public class GlobalAuroraTopologyMonitor extends ClusterTopologyMonitorImpl { + protected final Map instanceTemplatesByRegion; + protected final GlobalAuroraTopologyUtils topologyUtils; + + public GlobalAuroraTopologyMonitor( + final FullServicesContainer servicesContainer, + final GlobalAuroraTopologyUtils topologyUtils, + final String clusterId, + final HostSpec initialHostSpec, + final Properties properties, + final HostSpec instanceTemplate, + final long refreshRateNano, + final long highRefreshRateNano, + final Map instanceTemplatesByRegion) { + super(servicesContainer, + topologyUtils, + clusterId, + initialHostSpec, + properties, + instanceTemplate, + refreshRateNano, + highRefreshRateNano); + + this.instanceTemplatesByRegion = instanceTemplatesByRegion; + this.topologyUtils = topologyUtils; + } + + @Override + protected HostSpec getinstanceTemplate(String instanceId, Connection connection) throws SQLException { + String region = this.topologyUtils.getRegion(instanceId, connection); + if (!StringUtils.isNullOrEmpty(region)) { + final HostSpec instanceTemplate = this.instanceTemplatesByRegion.get(region); + if (instanceTemplate == null) { + throw new SQLException( + Messages.get("GlobalAuroraTopologyMonitor.cannotFindRegionTemplate", new Object[] {region})); + } + + return instanceTemplate; + } + + return this.instanceTemplate; + } + + @Override + protected List queryForTopology(Connection connection) throws SQLException { + return this.topologyUtils.queryForTopology(connection, this.initialHostSpec, this.instanceTemplatesByRegion); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalDbClusterTopologyMonitorImpl.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalDbClusterTopologyMonitorImpl.java deleted file mode 100644 index 0c2ee29a2..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/GlobalDbClusterTopologyMonitorImpl.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.hostlistprovider.monitoring; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.Map; -import java.util.Properties; -import java.util.logging.Logger; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.StringUtils; - - -public class GlobalDbClusterTopologyMonitorImpl extends ClusterTopologyMonitorImpl { - - private static final Logger LOGGER = Logger.getLogger(GlobalDbClusterTopologyMonitorImpl.class.getName()); - - protected final Map globalClusterInstanceTemplateByAwsRegion; - protected final String regionByNodeIdQuery; - - public GlobalDbClusterTopologyMonitorImpl( - final FullServicesContainer servicesContainer, - final String clusterId, - final HostSpec initialHostSpec, - final Properties properties, - final HostSpec clusterInstanceTemplate, - final long refreshRateNano, - final long highRefreshRateNano, - final String topologyQuery, - final String writerTopologyQuery, - final String nodeIdQuery, - final Map globalClusterInstanceTemplateByAwsRegion, - final String regionByNodeIdQuery) { - - super(servicesContainer, clusterId, initialHostSpec, properties, clusterInstanceTemplate, - refreshRateNano, highRefreshRateNano, topologyQuery, writerTopologyQuery, nodeIdQuery); - this.globalClusterInstanceTemplateByAwsRegion = globalClusterInstanceTemplateByAwsRegion; - this.regionByNodeIdQuery = regionByNodeIdQuery; - } - - @Override - protected HostSpec getClusterInstanceTemplate(String nodeId, Connection connection) { - try { - try (final PreparedStatement stmt = connection.prepareStatement(this.regionByNodeIdQuery)) { - stmt.setString(1, nodeId); - try (final ResultSet resultSet = stmt.executeQuery()) { - if (resultSet.next()) { - String awsRegion = resultSet.getString(1); - if (!StringUtils.isNullOrEmpty(awsRegion)) { - final HostSpec clusterInstanceTemplateForRegion - = this.globalClusterInstanceTemplateByAwsRegion.get(awsRegion); - if (clusterInstanceTemplateForRegion == null) { - throw new SQLException("Can't find cluster template for region " + awsRegion); - } - return clusterInstanceTemplateForRegion; - } - } - } - } - } catch (SQLException ex) { - throw new RuntimeException(ex); - } - return this.clusterInstanceTemplate; - } - - @Override - protected HostSpec createHost( - final ResultSet resultSet, - final String suggestedWriterNodeId) throws SQLException { - - // suggestedWriterNodeId is not used for Aurora Global Database clusters. - // Topology query can detect a writer for itself. - - // According to the topology query the result set - // should contain 4 columns: node ID, 1/0 (writer/reader), node lag in time (msec), AWS region. - String hostName = resultSet.getString(1); - final boolean isWriter = resultSet.getBoolean(2); - final float nodeLag = resultSet.getFloat(3); - final String awsRegion = resultSet.getString(4); - - // Calculate weight based on node lag in time and CPU utilization. - final long weight = Math.round(nodeLag) * 100L; - - final HostSpec clusterInstanceTemplateForRegion = this.globalClusterInstanceTemplateByAwsRegion.get(awsRegion); - if (clusterInstanceTemplateForRegion == null) { - throw new SQLException("Can't find cluster template for region " + awsRegion); - } - - return createHost( - hostName, hostName, isWriter, weight, Timestamp.from(Instant.now()), clusterInstanceTemplateForRegion); - } -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringGlobalAuroraHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringGlobalAuroraHostListProvider.java new file mode 100644 index 000000000..b258c223d --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringGlobalAuroraHostListProvider.java @@ -0,0 +1,91 @@ +/* + * 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.hostlistprovider.monitoring; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Logger; +import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.GlobalAuroraTopologyUtils; +import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; +import software.amazon.jdbc.util.Messages; +import software.amazon.jdbc.util.RdsUtils; +import software.amazon.jdbc.util.StringUtils; + +public class MonitoringGlobalAuroraHostListProvider extends MonitoringRdsHostListProvider { + + static final Logger LOGGER = Logger.getLogger(MonitoringGlobalAuroraHostListProvider.class.getName()); + + protected Map instanceTemplatesByRegion = new HashMap<>(); + + protected final RdsUtils rdsUtils = new RdsUtils(); + protected final GlobalAuroraTopologyUtils topologyUtils; + + static { + // Intentionally register property definition using the GlobalAuroraHostListProvider class. + PropertyDefinition.registerPluginProperties(GlobalAuroraHostListProvider.class); + } + + public MonitoringGlobalAuroraHostListProvider( + GlobalAuroraTopologyUtils topologyUtils, + Properties properties, + String originalUrl, + FullServicesContainer servicesContainer) { + super(topologyUtils, properties, originalUrl, servicesContainer); + this.topologyUtils = topologyUtils; + } + + @Override + protected void initSettings() throws SQLException { + super.initSettings(); + + String instanceTemplates = GlobalAuroraHostListProvider.GLOBAL_CLUSTER_INSTANCE_HOST_PATTERNS.getString(properties); + this.instanceTemplatesByRegion = + this.topologyUtils.parseInstanceTemplates(instanceTemplates, this::validateHostPatternSetting); + } + + protected ClusterTopologyMonitor initMonitor() throws SQLException { + return this.servicesContainer.getMonitorService().runIfAbsent( + ClusterTopologyMonitorImpl.class, + this.clusterId, + this.servicesContainer, + this.properties, + (servicesContainer) -> + new GlobalAuroraTopologyMonitor( + servicesContainer, + this.topologyUtils, + this.clusterId, + this.initialHostSpec, + this.properties, + this.instanceTemplate, + this.refreshRateNano, + this.highRefreshRateNano, + this.instanceTemplatesByRegion)); + } + + @Override + protected List queryForTopology(Connection connection) throws SQLException { + return this.topologyUtils.queryForTopology(connection, this.initialHostSpec, this.instanceTemplatesByRegion); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsHostListProvider.java index f3a39b7a9..a1ab3490c 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsHostListProvider.java +++ b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsHostListProvider.java @@ -22,7 +22,6 @@ import java.util.Properties; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.logging.Logger; import software.amazon.jdbc.AwsWrapperProperty; import software.amazon.jdbc.BlockingHostListProvider; import software.amazon.jdbc.HostSpec; @@ -30,15 +29,11 @@ import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.cleanup.CanReleaseResources; import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; -import software.amazon.jdbc.hostlistprovider.Topology; +import software.amazon.jdbc.hostlistprovider.TopologyUtils; import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.monitoring.MonitorService; -import software.amazon.jdbc.util.storage.StorageService; -public class MonitoringRdsHostListProvider extends RdsHostListProvider - implements BlockingHostListProvider, CanReleaseResources { - - private static final Logger LOGGER = Logger.getLogger(MonitoringRdsHostListProvider.class.getName()); +public class MonitoringRdsHostListProvider + extends RdsHostListProvider implements BlockingHostListProvider, CanReleaseResources { public static final AwsWrapperProperty CLUSTER_TOPOLOGY_HIGH_REFRESH_RATE_MS = new AwsWrapperProperty( @@ -53,29 +48,19 @@ public class MonitoringRdsHostListProvider extends RdsHostListProvider protected final FullServicesContainer servicesContainer; protected final PluginService pluginService; protected final long highRefreshRateNano; - protected final String writerTopologyQuery; public MonitoringRdsHostListProvider( + final TopologyUtils topologyUtils, final Properties properties, final String originalUrl, - final FullServicesContainer servicesContainer, - final String topologyQuery, - final String nodeIdQuery, - final String isReaderQuery, - final String writerTopologyQuery) { - super(properties, originalUrl, servicesContainer, topologyQuery, nodeIdQuery, isReaderQuery); + final FullServicesContainer servicesContainer) { + super(topologyUtils, properties, originalUrl, servicesContainer); this.servicesContainer = servicesContainer; this.pluginService = servicesContainer.getPluginService(); - this.writerTopologyQuery = writerTopologyQuery; this.highRefreshRateNano = TimeUnit.MILLISECONDS.toNanos( CLUSTER_TOPOLOGY_HIGH_REFRESH_RATE_MS.getLong(this.properties)); } - @Override - protected void init() throws SQLException { - super.init(); - } - protected ClusterTopologyMonitor initMonitor() throws SQLException { return this.servicesContainer.getMonitorService().runIfAbsent( ClusterTopologyMonitorImpl.class, @@ -83,16 +68,14 @@ protected ClusterTopologyMonitor initMonitor() throws SQLException { this.servicesContainer, this.properties, (servicesContainer) -> new ClusterTopologyMonitorImpl( - servicesContainer, + this.servicesContainer, + this.topologyUtils, this.clusterId, this.initialHostSpec, this.properties, - this.clusterInstanceTemplate, + this.instanceTemplate, this.refreshRateNano, - this.highRefreshRateNano, - this.topologyQuery, - this.writerTopologyQuery, - this.nodeIdQuery)); + this.highRefreshRateNano)); } @Override @@ -125,6 +108,6 @@ public List forceRefresh(final boolean shouldVerifyWriter, final long @Override public void releaseResources() { - // do nothing + // Do nothing. } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsMultiAzHostListProvider.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsMultiAzHostListProvider.java deleted file mode 100644 index c11da2be9..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MonitoringRdsMultiAzHostListProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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.hostlistprovider.monitoring; - -import java.sql.SQLException; -import java.util.Properties; -import java.util.logging.Logger; -import software.amazon.jdbc.util.FullServicesContainer; - -public class MonitoringRdsMultiAzHostListProvider extends MonitoringRdsHostListProvider { - - private static final Logger LOGGER = Logger.getLogger(MonitoringRdsMultiAzHostListProvider.class.getName()); - - protected final String fetchWriterNodeQuery; - protected final String fetchWriterNodeColumnName; - - public MonitoringRdsMultiAzHostListProvider( - final Properties properties, - final String originalUrl, - final FullServicesContainer servicesContainer, - final String topologyQuery, - final String nodeIdQuery, - final String isReaderQuery, - final String fetchWriterNodeQuery, - final String fetchWriterNodeColumnName) { - super( - properties, - originalUrl, - servicesContainer, - topologyQuery, - nodeIdQuery, - isReaderQuery, - ""); - this.fetchWriterNodeQuery = fetchWriterNodeQuery; - this.fetchWriterNodeColumnName = fetchWriterNodeColumnName; - } - - @Override - protected ClusterTopologyMonitor initMonitor() throws SQLException { - return this.servicesContainer.getMonitorService().runIfAbsent(MultiAzClusterTopologyMonitorImpl.class, - this.clusterId, - this.servicesContainer, - this.properties, - (servicesContainer) -> new MultiAzClusterTopologyMonitorImpl( - servicesContainer, - this.clusterId, - this.initialHostSpec, - this.properties, - this.hostListProviderService, - this.clusterInstanceTemplate, - this.refreshRateNano, - this.highRefreshRateNano, - this.topologyQuery, - this.writerTopologyQuery, - this.nodeIdQuery, - this.fetchWriterNodeQuery, - this.fetchWriterNodeColumnName)); - } - -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MultiAzClusterTopologyMonitorImpl.java b/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MultiAzClusterTopologyMonitorImpl.java deleted file mode 100644 index de23fb34c..000000000 --- a/wrapper/src/main/java/software/amazon/jdbc/hostlistprovider/monitoring/MultiAzClusterTopologyMonitorImpl.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.hostlistprovider.monitoring; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; -import java.util.Properties; -import java.util.logging.Logger; -import software.amazon.jdbc.HostListProviderService; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.StringUtils; - -public class MultiAzClusterTopologyMonitorImpl extends ClusterTopologyMonitorImpl { - - private static final Logger LOGGER = Logger.getLogger(MultiAzClusterTopologyMonitorImpl.class.getName()); - - protected final String fetchWriterNodeQuery; - protected final String fetchWriterNodeColumnName; - - public MultiAzClusterTopologyMonitorImpl( - final FullServicesContainer servicesContainer, - final String clusterId, - final HostSpec initialHostSpec, - final Properties properties, - final HostListProviderService hostListProviderService, - final HostSpec clusterInstanceTemplate, - final long refreshRateNano, - final long highRefreshRateNano, - final String topologyQuery, - final String writerTopologyQuery, - final String nodeIdQuery, - final String fetchWriterNodeQuery, - final String fetchWriterNodeColumnName) { - super( - servicesContainer, - clusterId, - initialHostSpec, - properties, - clusterInstanceTemplate, - refreshRateNano, - highRefreshRateNano, - topologyQuery, - writerTopologyQuery, - nodeIdQuery); - this.fetchWriterNodeQuery = fetchWriterNodeQuery; - this.fetchWriterNodeColumnName = fetchWriterNodeColumnName; - } - - // Returns a writer node ID if connected to a writer node. Returns null otherwise. - @Override - protected String getWriterNodeId(final Connection connection) throws SQLException { - try (final Statement stmt = connection.createStatement()) { - try (final ResultSet resultSet = stmt.executeQuery(this.fetchWriterNodeQuery)) { - if (resultSet.next()) { - String nodeId = resultSet.getString(this.fetchWriterNodeColumnName); - if (!StringUtils.isNullOrEmpty(nodeId)) { - // Replica status exists and shows a writer node ID. - // That means that this node (this connection) is a reader - return null; - } - } - } - // Replica status doesn't exist. That means that this node is a writer. - try (final ResultSet resultSet = stmt.executeQuery(this.nodeIdQuery)) { - if (resultSet.next()) { - return resultSet.getString(1); - } - } - } - return null; - } - - @Override - protected String getSuggestedWriterNodeId(final Connection connection) throws SQLException { - try (final Statement stmt = connection.createStatement()) { - try (final ResultSet resultSet = stmt.executeQuery(this.fetchWriterNodeQuery)) { - if (resultSet.next()) { - String nodeId = resultSet.getString(this.fetchWriterNodeColumnName); - if (!StringUtils.isNullOrEmpty(nodeId)) { - // Replica status exists and shows a writer node ID. - // That means that this node (this connection) is a reader. - // But we now what replication source is and that is a writer node. - return nodeId; - } - } - } - // Replica status doesn't exist. That means that this node is a writer. - try (final ResultSet resultSet = stmt.executeQuery(this.nodeIdQuery)) { - if (resultSet.next()) { - return resultSet.getString(1); - } - } - } - return null; - } - - @Override - protected HostSpec createHost( - final ResultSet resultSet, - final String suggestedWriterNodeId) throws SQLException { - - String endpoint = resultSet.getString("endpoint"); // "instance-name.XYZ.us-west-2.rds.amazonaws.com" - String instanceName = endpoint.substring(0, endpoint.indexOf(".")); // "instance-name" - String hostId = resultSet.getString("id"); // "1034958454" - final boolean isWriter = hostId.equals(suggestedWriterNodeId); - - return createHost(hostId, instanceName, isWriter, 0, Timestamp.from(Instant.now()), this.clusterInstanceTemplate); - } -} diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java index 035e4ecf9..9cfb8de24 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AbstractConnectionPlugin.java @@ -24,12 +24,12 @@ import java.util.Properties; import java.util.Set; import software.amazon.jdbc.ConnectionPlugin; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; import software.amazon.jdbc.NodeChangeOptions; import software.amazon.jdbc.OldConnectionSuggestedAction; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; public abstract class AbstractConnectionPlugin implements ConnectionPlugin { diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java index 1dbee4029..9e692f777 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AuroraInitialConnectionStrategyPlugin.java @@ -30,13 +30,13 @@ import java.util.stream.Collectors; import org.jetbrains.annotations.Nullable; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java index 2427cc816..8da199b59 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/DefaultConnectionPlugin.java @@ -35,7 +35,6 @@ import software.amazon.jdbc.ConnectionPlugin; import software.amazon.jdbc.ConnectionProvider; import software.amazon.jdbc.ConnectionProviderManager; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; @@ -44,6 +43,7 @@ import software.amazon.jdbc.PluginManagerService; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlMethodAnalyzer; import software.amazon.jdbc.util.WrapperUtils; diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenInterimStatus.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenInterimStatus.java index 63a2cef2a..4a49d57a6 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenInterimStatus.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenInterimStatus.java @@ -23,8 +23,8 @@ import java.util.Set; import java.util.stream.Collectors; import software.amazon.jdbc.HostSpec; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.StringUtils; -import software.amazon.jdbc.util.Utils; public class BlueGreenInterimStatus { public BlueGreenPhase blueGreenPhase; @@ -74,8 +74,8 @@ public String toString() { .map(x -> String.format("%s -> %s", x.getKey(), x.getValue())) .collect(Collectors.joining("\n ")); String allHostNamesStr = String.join("\n ", this.hostNames); - String startTopologyStr = Utils.logTopology(this.startTopology); - String currentTopologyStr = Utils.logTopology(this.currentTopology); + String startTopologyStr = LogUtils.logTopology(this.startTopology); + String currentTopologyStr = LogUtils.logTopology(this.currentTopology); return String.format("%s [\n" + " phase %s, \n" + " version '%s', \n" diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenStatusMonitor.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenStatusMonitor.java index 057b152a6..832e06056 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenStatusMonitor.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/bluegreen/BlueGreenStatusMonitor.java @@ -43,12 +43,12 @@ import java.util.stream.Collectors; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import software.amazon.jdbc.HostListProvider; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.dialect.BlueGreenDialect; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.iam.IamAuthConnectionPlugin; import software.amazon.jdbc.util.ConnectionUrlParser; @@ -626,7 +626,7 @@ protected void initHostListProvider() { if (connectionHostSpecCopy != null) { String hostListProviderUrl = String.format("%s%s/", protocol, connectionHostSpecCopy.getHostAndPort()); this.hostListProvider = this.pluginService.getDialect() - .getHostListProvider() + .getHostListProviderSupplier() .getProvider( hostListProperties, hostListProviderUrl, diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareReaderFailoverHandler.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareReaderFailoverHandler.java index 6b8661e02..1b5078d26 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareReaderFailoverHandler.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareReaderFailoverHandler.java @@ -317,7 +317,7 @@ private ReaderFailoverResult getConnectionFromHostGroup(final List hos } } - return new ReaderFailoverResult(null, null, false); + return FAILED_READER_FAILOVER_RESULT; } finally { executor.shutdownNow(); } @@ -364,7 +364,7 @@ private ReaderFailoverResult getResultFromNextTaskBatch( return result; } } - return new ReaderFailoverResult(null, null, false); + return FAILED_READER_FAILOVER_RESULT; } private ReaderFailoverResult getNextResult(final CompletionService service) diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandler.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandler.java index 8504842c6..a6eafd1d3 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandler.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover/ClusterAwareWriterFailoverHandler.java @@ -37,6 +37,7 @@ import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.util.ExecutorFactory; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; import software.amazon.jdbc.util.ServiceUtility; @@ -465,7 +466,7 @@ private boolean refreshTopologyAndConnectToNewWriter() throws InterruptedExcepti if (allowOldWriter || !isSame(writerCandidate, this.originalWriterHost)) { // new writer is available, and it's different from the previous writer - LOGGER.finest(() -> Utils.logTopology(this.currentTopology, "[TaskB] Topology:")); + LOGGER.finest(() -> LogUtils.logTopology(this.currentTopology, "[TaskB] Topology:")); if (connectToWriter(writerCandidate)) { return true; } 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 25ce77ecb..0eb2d9baa 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 @@ -33,7 +33,6 @@ import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; @@ -43,10 +42,12 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsHelper; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; @@ -757,7 +758,7 @@ protected void failoverWriter() throws SQLException { throwFailoverFailedException( Messages.get( "Failover.noWriterHostAfterReconnecting", - new Object[]{Utils.logTopology(hosts, "")})); + new Object[]{LogUtils.logTopology(hosts, "")})); return; } @@ -765,7 +766,7 @@ protected void failoverWriter() throws SQLException { if (!Utils.containsHostAndPort(allowedHosts, writerHostSpec.getHostAndPort())) { throwFailoverFailedException( Messages.get("Failover.newWriterNotAllowed", - new Object[] {writerHostSpec.getUrl(), Utils.logTopology(allowedHosts, "")})); + new Object[] {writerHostSpec.getUrl(), LogUtils.logTopology(allowedHosts, "")})); return; } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover2/FailoverConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover2/FailoverConnectionPlugin.java index bd1d6b660..3c4e109a8 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/failover2/FailoverConnectionPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/failover2/FailoverConnectionPlugin.java @@ -31,7 +31,6 @@ import java.util.logging.Logger; import java.util.stream.Collectors; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; @@ -40,6 +39,7 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.hostavailability.HostAvailability; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverFailedSQLException; import software.amazon.jdbc.plugin.failover.FailoverMode; @@ -47,6 +47,7 @@ import software.amazon.jdbc.plugin.failover.TransactionStateUnknownSQLException; import software.amazon.jdbc.plugin.staledns.AuroraStaleDnsHelper; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; @@ -430,7 +431,7 @@ protected ReaderFailoverResult getReaderFailoverConnection(long failoverEndTimeN this.failoverReaderHostSelectorStrategySetting); } catch (UnsupportedOperationException | SQLException ex) { LOGGER.finest( - Utils.logTopology( + LogUtils.logTopology( new ArrayList<>(remainingReaders), Messages.get("Failover.errorSelectingReaderHost", new Object[]{ex.getMessage()}))); break; @@ -438,7 +439,7 @@ protected ReaderFailoverResult getReaderFailoverConnection(long failoverEndTimeN if (readerCandidate == null) { LOGGER.finest( - Utils.logTopology(new ArrayList<>(remainingReaders), Messages.get("Failover.readerCandidateNull"))); + LogUtils.logTopology(new ArrayList<>(remainingReaders), Messages.get("Failover.readerCandidateNull"))); break; } @@ -559,7 +560,7 @@ protected void failoverWriter() throws SQLException { if (this.failoverWriterFailedCounter != null) { this.failoverWriterFailedCounter.inc(); } - String message = Utils.logTopology(updatedHosts, Messages.get("Failover.noWriterHost")); + String message = LogUtils.logTopology(updatedHosts, Messages.get("Failover.noWriterHost")); LOGGER.severe(message); throw new FailoverFailedSQLException(message); } @@ -569,7 +570,7 @@ protected void failoverWriter() throws SQLException { if (this.failoverWriterFailedCounter != null) { this.failoverWriterFailedCounter.inc(); } - String topologyString = Utils.logTopology(allowedHosts, ""); + String topologyString = LogUtils.logTopology(allowedHosts, ""); LOGGER.severe(Messages.get("Failover.newWriterNotAllowed", new Object[] {writerCandidate.getUrl(), topologyString})); throw new FailoverFailedSQLException( diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterMonitor.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterMonitor.java index f4075a285..9264e1603 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterMonitor.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterMonitor.java @@ -26,9 +26,9 @@ import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.PropertyUtils; -import software.amazon.jdbc.util.Utils; import software.amazon.jdbc.util.monitoring.AbstractMonitor; import software.amazon.jdbc.util.storage.StorageService; import software.amazon.jdbc.util.telemetry.TelemetryContext; @@ -116,7 +116,7 @@ public void monitor() { List newLimitlessRouters = queryHelper.queryForLimitlessRouters(this.monitoringConn, this.hostSpec.getPort()); this.storageService.set(this.limitlessRouterCacheKey, new LimitlessRouters(newLimitlessRouters)); - LOGGER.finest(Utils.logTopology(newLimitlessRouters, "[limitlessRouterMonitor] Topology:")); + LOGGER.finest(LogUtils.logTopology(newLimitlessRouters, "[limitlessRouterMonitor] Topology:")); TimeUnit.MILLISECONDS.sleep(this.intervalMs); // do not include this in the telemetry } catch (final Exception ex) { if (telemetryContext != null) { diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPlugin.java index 5975936c0..6fff83f53 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPlugin.java @@ -28,7 +28,6 @@ import java.util.logging.Logger; import org.checkerframework.checker.nullness.qual.NonNull; import software.amazon.jdbc.AwsWrapperProperty; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; @@ -38,8 +37,10 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.cleanup.CanReleaseResources; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverSQLException; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.SqlState; import software.amazon.jdbc.util.Utils; @@ -425,7 +426,7 @@ private void switchToReaderConnection(final List hosts) LOGGER.finest( Messages.get( "ReadWriteSplittingPlugin.previousReaderNotAllowed", - new Object[] {this.readerHostSpec, Utils.logTopology(hosts, "")})); + new Object[] {this.readerHostSpec, LogUtils.logTopology(hosts, "")})); closeConnectionIfIdle(this.readerConnection); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsHelper.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsHelper.java index 69864104d..a54e345d5 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsHelper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsHelper.java @@ -25,12 +25,13 @@ import java.util.Map; import java.util.Properties; import java.util.logging.Logger; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; import software.amazon.jdbc.NodeChangeOptions; import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; +import software.amazon.jdbc.util.LogUtils; import software.amazon.jdbc.util.Messages; import software.amazon.jdbc.util.RdsUrlType; import software.amazon.jdbc.util.RdsUtils; @@ -101,7 +102,7 @@ public Connection getVerifiedConnection( this.pluginService.refreshHostList(conn); } - LOGGER.finest(() -> Utils.logTopology(this.pluginService.getAllHosts())); + LOGGER.finest(() -> LogUtils.logTopology(this.pluginService.getAllHosts())); if (this.writerHostSpec == null) { final HostSpec writerCandidate = Utils.getWriter(this.pluginService.getAllHosts()); @@ -149,7 +150,7 @@ public Connection getVerifiedConnection( Messages.get("AuroraStaleDnsHelper.currentWriterNotAllowed", new Object[] { this.writerHostSpec == null ? "" : this.writerHostSpec.getHostAndPort(), - Utils.logTopology(allowedHosts, "")}) + LogUtils.logTopology(allowedHosts, "")}) ); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsPlugin.java index 6345c27fa..080d19990 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsPlugin.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/staledns/AuroraStaleDnsPlugin.java @@ -25,12 +25,12 @@ import java.util.Properties; import java.util.Set; import java.util.logging.Logger; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.JdbcCallable; import software.amazon.jdbc.JdbcMethod; import software.amazon.jdbc.NodeChangeOptions; import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.AbstractConnectionPlugin; /** diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java index 9915391ac..4d90f6d5d 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/strategy/fastestresponse/HostResponseTimeServiceImpl.java @@ -28,7 +28,6 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.util.FullServicesContainer; import software.amazon.jdbc.util.Messages; -import software.amazon.jdbc.util.storage.SlidingExpirationCacheWithCleanupThread; public class HostResponseTimeServiceImpl implements HostResponseTimeService { diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainer.java b/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainer.java index 7b7857175..4420bab7d 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainer.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainer.java @@ -18,9 +18,9 @@ import software.amazon.jdbc.ConnectionPluginManager; import software.amazon.jdbc.ConnectionProvider; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.PluginManagerService; import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.util.monitoring.MonitorService; import software.amazon.jdbc.util.storage.StorageService; import software.amazon.jdbc.util.telemetry.TelemetryFactory; diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainerImpl.java b/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainerImpl.java index db0ea3f57..44cfda664 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainerImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/FullServicesContainerImpl.java @@ -18,9 +18,9 @@ import software.amazon.jdbc.ConnectionPluginManager; import software.amazon.jdbc.ConnectionProvider; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.PluginManagerService; import software.amazon.jdbc.PluginService; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.util.monitoring.MonitorService; import software.amazon.jdbc.util.storage.StorageService; import software.amazon.jdbc.util.telemetry.TelemetryFactory; diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/LogUtils.java b/wrapper/src/main/java/software/amazon/jdbc/util/LogUtils.java new file mode 100644 index 000000000..932e4a21e --- /dev/null +++ b/wrapper/src/main/java/software/amazon/jdbc/util/LogUtils.java @@ -0,0 +1,55 @@ +/* + * 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.util; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.checkerframework.checker.nullness.qual.Nullable; +import software.amazon.jdbc.HostSpec; + +public class LogUtils { + public static String logTopology(final @Nullable List hosts) { + return logTopology(hosts, null); + } + + public static String logTopology( + final @Nullable List hosts, + final @Nullable String messagePrefix) { + + final StringBuilder msg = new StringBuilder(); + if (hosts == null) { + msg.append(""); + } else { + for (final HostSpec host : hosts) { + if (msg.length() > 0) { + msg.append("\n"); + } + msg.append(" ").append(host == null ? "" : host); + } + } + + return Messages.get("Utils.topology", + new Object[] {messagePrefix == null ? "Topology:" : messagePrefix, msg.toString()}); + } + + public static String toLogString(Map map) { + return map.entrySet().stream() + .map(x -> String.format("\t[%s] -> %s", x.getKey(), x.getValue().getHostAndPort())) + .collect(Collectors.joining("\n")); + } +} diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/ServiceUtility.java b/wrapper/src/main/java/software/amazon/jdbc/util/ServiceUtility.java index fe150f835..3e4b134a1 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/ServiceUtility.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/ServiceUtility.java @@ -21,11 +21,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.ConnectionPluginManager; import software.amazon.jdbc.ConnectionProvider; -import software.amazon.jdbc.HostListProvider; import software.amazon.jdbc.PartialPluginService; import software.amazon.jdbc.PluginServiceImpl; import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.dialect.HostListProviderSupplier; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.monitoring.MonitorService; @@ -73,7 +73,7 @@ public FullServicesContainer createStandardServiceContainer( servicesContainer.setPluginManagerService(pluginService); pluginManager.initPlugins(servicesContainer, configurationProfile); - final HostListProviderSupplier supplier = pluginService.getDialect().getHostListProvider(); + final HostListProviderSupplier supplier = pluginService.getDialect().getHostListProviderSupplier(); if (supplier != null) { final HostListProvider provider = supplier.getProvider(props, originalUrl, servicesContainer); pluginService.setHostListProvider(provider); diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/Utils.java b/wrapper/src/main/java/software/amazon/jdbc/util/Utils.java index 4d02e9224..8bfe5ca1b 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/Utils.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/Utils.java @@ -53,28 +53,4 @@ public static boolean containsHostAndPort(final Collection hosts, Stri } return null; } - - public static String logTopology(final @Nullable List hosts) { - return logTopology(hosts, null); - } - - public static String logTopology( - final @Nullable List hosts, - final @Nullable String messagePrefix) { - - final StringBuilder msg = new StringBuilder(); - if (hosts == null) { - msg.append(""); - } else { - for (final HostSpec host : hosts) { - if (msg.length() > 0) { - msg.append("\n"); - } - msg.append(" ").append(host == null ? "" : host); - } - } - - return Messages.get("Utils.topology", - new Object[] {messagePrefix == null ? "Topology:" : messagePrefix, msg.toString()}); - } } diff --git a/wrapper/src/main/java/software/amazon/jdbc/util/monitoring/MonitorServiceImpl.java b/wrapper/src/main/java/software/amazon/jdbc/util/monitoring/MonitorServiceImpl.java index 97de15f57..509b3c8fd 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/util/monitoring/MonitorServiceImpl.java +++ b/wrapper/src/main/java/software/amazon/jdbc/util/monitoring/MonitorServiceImpl.java @@ -36,7 +36,6 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.hostlistprovider.Topology; import software.amazon.jdbc.hostlistprovider.monitoring.ClusterTopologyMonitorImpl; -import software.amazon.jdbc.hostlistprovider.monitoring.MultiAzClusterTopologyMonitorImpl; import software.amazon.jdbc.plugin.strategy.fastestresponse.NodeResponseTimeMonitor; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.ExecutorFactory; @@ -64,7 +63,6 @@ public class MonitorServiceImpl implements MonitorService, EventSubscriber { TimeUnit.MINUTES.toNanos(15), TimeUnit.MINUTES.toNanos(3), recreateOnError); suppliers.put(ClusterTopologyMonitorImpl.class, () -> new CacheContainer(defaultSettings, Topology.class)); - suppliers.put(MultiAzClusterTopologyMonitorImpl.class, () -> new CacheContainer(defaultSettings, Topology.class)); suppliers.put(NodeResponseTimeMonitor.class, () -> new CacheContainer(defaultSettings, null)); defaultSuppliers = Collections.unmodifiableMap(suppliers); } diff --git a/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java b/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java index b0e86574e..312dd8c26 100644 --- a/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java +++ b/wrapper/src/main/java/software/amazon/jdbc/wrapper/ConnectionWrapper.java @@ -39,12 +39,12 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import software.amazon.jdbc.ConnectionPluginManager; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.JdbcMethod; import software.amazon.jdbc.PluginManagerService; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.cleanup.CanReleaseResources; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.util.FullServicesContainer; import software.amazon.jdbc.util.Messages; 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 dbfc72a4b..441188f6f 100644 --- a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties +++ b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties @@ -27,6 +27,8 @@ AdfsCredentialsProviderFactory.signOnPagePostActionRequestFailed=ADFS SignOn Pag AdfsCredentialsProviderFactory.signOnPageRequestFailed=ADFS SignOn Page Request Failed with HTTP status ''{0}'', reason phrase ''{1}'', and response ''{2}'' AdfsCredentialsProviderFactory.signOnPageUrl=ADFS SignOn URL: ''{0}'' +AuroraPgDialect.auroraUtils=auroraUtils: {0} + AuthenticationToken.useCachedToken=Use cached authentication token = ''{0}'' AuthenticationToken.generatedNewToken=Generated new authentication token = ''{0}'' AuthenticationToken.javaSdkNotInClasspath=Required dependency 'AWS Java SDK RDS v2.x' is not on the classpath. @@ -34,13 +36,10 @@ AuthenticationToken.javaSdkNotInClasspath=Required dependency 'AWS Java SDK RDS RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRDSProxy=An RDS Proxy url can''t be used as the 'clusterInstanceHostPattern' configuration setting. RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRdsCustom=A custom RDS url can''t be used as the 'clusterInstanceHostPattern' configuration setting. RdsHostListProvider.invalidPattern=Invalid value for the 'clusterInstanceHostPattern' configuration setting - the host pattern must contain a '?' character as a placeholder for the DB instance identifiers of the instances in the cluster. -RdsHostListProvider.invalidTopology=The topology query returned an invalid topology - no writer instance detected. -RdsHostListProvider.suggestedClusterId=ClusterId ''{0}'' is suggested for url ''{1}''. RdsHostListProvider.parsedListEmpty=Can''t parse connection string: ''{0}'' -RdsHostListProvider.invalidQuery=Error obtaining host list. Provided database might not be an Aurora Db cluster -RdsHostListProvider.errorGettingHostRole=An error occurred while obtaining the connected host's role. This could occur if the connection is broken or if you are not connected to an Aurora database. RdsHostListProvider.errorIdentifyConnection=An error occurred while obtaining the connection's host ID. -RdsHostListProvider.errorGettingNetworkTimeout=An error occurred while getting the connection network timeout: {0} + +RdsPgDialect.rdsToolsAuroraUtils=rdsTools: {0}, auroraUtils: {1} AwsSdk.unsupportedRegion=Unsupported AWS region ''{0}''. For supported regions please read https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html @@ -135,6 +134,8 @@ DefaultConnectionPlugin.executingMethod=Executing method: ''{0}'' DefaultConnectionPlugin.noHostsAvailable=The default connection plugin received an empty host list from the plugin service. DefaultConnectionPlugin.unknownRoleRequested=A HostSpec with a role of HostRole.UNKNOWN was requested via getHostSpecByStrategy. The requested role must be either HostRole.WRITER or HostRole.READER +DialectManager.currentDialect=Current dialect: {0}, {1}, canUpdate: {2} + Driver.nullUrl=Url is null. Driver.alreadyRegistered=Driver is already registered. It can only be registered once. Driver.missingDriver=Can''t find the target driver for ''{0}''. Please ensure the target driver is in the classpath and is registered. Here is the list of registered drivers in the classpath: {1} @@ -184,6 +185,11 @@ Failover.skipFailoverOnInterruptedThread=Do not start failover since the current FederatedAuthPlugin.unableToDetermineRegion=Unable to determine connection region. If you are using a non-standard RDS URL, please set the ''{0}'' property. +GlobalAuroraTopologyMonitor.cannotFindRegionTemplate=Cannot find cluster template for region {0}. + +GlobalAuroraTopologyUtils.globalClusterInstanceHostPatternsRequired=Parameter 'globalClusterInstanceHostPatterns' is required for Aurora Global Database. +GlobalAuroraTopologyUtils.detectedGdbPatterns=Detected GDB instance template patterns:\n{0} + HostAvailabilityStrategy.invalidMaxRetries=Invalid value of {0} for configuration parameter `hostAvailabilityStrategyMaxRetries`. It must be an integer greater than 1. HostAvailabilityStrategy.invalidInitialBackoffTime=Invalid value of {0} for configuration parameter `hostAvailabilityStrategyInitialBackoffTime`. It must be an integer greater than 1. @@ -250,7 +256,6 @@ HostMonitorImpl.interruptedExceptionDuringMonitoring=Monitoring thread for node HostMonitorImpl.exceptionDuringMonitoringContinue=Continuing monitoring after unhandled exception was thrown in monitoring thread for node {0}. HostMonitorImpl.exceptionDuringMonitoringStop=Stopping monitoring after unhandled exception was thrown in monitoring thread for node {0}. HostMonitorImpl.monitorIsStopped=Monitoring was already stopped for node {0}. -HostMonitorImpl.stopped=Stopped monitoring thread for node ''{0}''. HostMonitorImpl.startMonitoringThreadNewContext=Start monitoring thread for checking new contexts for {0}. HostMonitorImpl.stopMonitoringThreadNewContext=Stop monitoring thread for checking new contexts for {0}. HostMonitorImpl.startMonitoringThread=Start monitoring thread for {0}. @@ -260,6 +265,8 @@ HostMonitorServiceImpl.emptyAliasSet=Empty alias set passed for ''{0}''. Set sho HostResponseTimeServiceImpl.errorStartingMonitor=An error occurred while starting a response time monitor for ''{0}'': {1} +MonitoringGlobalAuroraHostListProvider.globalHostPatternsRequired=Parameter 'globalClusterInstanceHostPatterns' is required for Aurora Global Database. + MonitorServiceImpl.checkingMonitors=Checking monitors for errors... MonitorServiceImpl.monitorClassMismatch=The monitor stored at ''{0}'' did not have the expected type. The expected type was ''{1}'', but the monitor ''{2}'' had a type of ''{3}''. MonitorServiceImpl.monitorStuck=Monitor ''{0}'' has not been updated within the inactive timeout of {1} milliseconds. The monitor will be stopped. @@ -358,11 +365,18 @@ TargetDriverDialectManager.useDialect=Target driver dialect set to: ''{0}'', {1} TargetDriverDialectManager.unexpectedClass=Unexpected DataSource class. Expected class(es): {0}, actual class: {1}. TargetDriverDialect.unsupported=This target driver dialect does not support this operation. MysqlConnectorJDriverHelper.canNotRegister=Can''t register driver com.mysql.cj.jdbc.Driver. + +TopologyUtils.errorGettingHostRole=An error occurred while obtaining the connected host's role. This could occur if the connection is broken or if you are not connected to an Aurora database. +TopologyUtils.errorGettingNetworkTimeout=An error occurred while getting the connection network timeout: {0} +TopologyUtils.errorProcessingQueryResults=An error occurred while processing the results from the topology query: {0} +TopologyUtils.invalidQuery=An error occurred while attempting to obtain the topology because the topology query was invalid. Please ensure you are connecting to an Aurora or RDS cluster. +TopologyUtils.invalidTopology=The topology query returned an invalid topology - no writer instance detected. +TopologyUtils.unexpectedTopologyQueryColumnCount=The topology query returned a result with 0 columns. This may occur if the topology query is executed when the server is failing over. + MariadbDriverHelper.canNotRegister=Can''t register driver org.mariadb.jdbc.Driver. AuroraInitialConnectionStrategyPlugin.unsupportedStrategy=Unsupported host selection strategy ''{0}''. -NodeResponseTimeMonitor.stopped=Stopped Response time thread for node ''{0}''. NodeResponseTimeMonitor.responseTime=Response time for ''{0}'': {1} ms NodeResponseTimeMonitor.interruptedExceptionDuringMonitoring=Response time thread for node {0} was interrupted. NodeResponseTimeMonitor.exceptionDuringMonitoringStop=Stopping thread after unhandled exception was thrown in Response time thread for node {0}. @@ -372,10 +386,7 @@ NodeResponseTimeMonitor.openedConnection=Opened Response time connection: {0}. ClusterTopologyMonitorImpl.startMonitoringThread=[clusterId: ''{0}''] Start cluster topology monitoring thread for ''{1}''. ClusterTopologyMonitorImpl.stopMonitoringThread=Stop cluster topology monitoring thread for ''{0}''. ClusterTopologyMonitorImpl.exceptionDuringMonitoringStop=Stopping cluster topology monitoring after unhandled exception was thrown in monitoring thread for node ''{0}''. -ClusterTopologyMonitorImpl.invalidQuery=An error occurred while attempting to obtain the topology because the topology query was invalid. Please ensure you are connecting to an Aurora or RDS Db cluster. -ClusterTopologyMonitorImpl.errorGettingNetworkTimeout=An error occurred while getting the connection network timeout: {0} -ClusterTopologyMonitorImpl.invalidTopology=The topology query returned an invalid topology - no writer instance detected. -ClusterTopologyMonitorImpl.topologyNotUpdated=Topology hasn''t been updated after {0} ms. +ClusterTopologyMonitorImpl.topologyNotUpdated=Topology has not been updated after {0} ms. ClusterTopologyMonitorImpl.openedMonitoringConnection=Opened monitoring connection to node ''{0}''. ClusterTopologyMonitorImpl.ignoringTopologyRequest=A topology refresh was requested, but the topology was already updated recently. Returning cached hosts: ClusterTopologyMonitorImpl.timeoutSetToZero=A topology refresh was requested, but the given timeout for the request was 0ms. Returning cached hosts: @@ -384,8 +395,6 @@ ClusterTopologyMonitorImpl.startingNodeMonitoringThreads=Starting node monitorin ClusterTopologyMonitorImpl.writerPickedUpFromNodeMonitors=The writer host detected by the node monitors was picked up by the topology monitor: ''{0}''. ClusterTopologyMonitorImpl.writerMonitoringConnection=The monitoring connection is connected to a writer: ''{0}''. ClusterTopologyMonitorImpl.errorFetchingTopology=An error occurred while querying for topology: {0} -ClusterTopologyMonitorImpl.errorProcessingQueryResults=An error occurred while processing the results from the topology query: {0} -ClusterTopologyMonitorImpl.unexpectedTopologyQueryColumnCount=The topology query returned a result with 0 columns. This may occur if the topology query is executed when the server is failing over. # Blue/Green Deployment bgd.inProgressConnectionClosed=Connection has been closed since Blue/Green switchover is in progress. @@ -406,7 +415,7 @@ bgd.interrupted=[{0}] Interrupted. bgd.monitoringUnhandledException=[{0}] Unhandled exception while monitoring blue/green status. bgd.threadCompleted=[{0}] Blue/green status monitoring thread is completed. bgd.statusNotAvailable=[{0}] (status not available) currentPhase: {1} -bgd.usesVersion=[{0}] Blue/Green deployment uses version ''{1}'' which the driver doesn''t support. Version ''{2}'' will be used instead. +bgd.usesVersion=[{0}] Blue/Green deployment uses version ''{1}'' which the driver does not support. Version ''{2}'' will be used instead. bgd.noEntriesInStatusTable=[{0}] No entries in status table. bgd.exception=[{0}] currentPhase: {1}, exception while querying for blue/green status. bgd.unhandledSqlException=[{0}] Unhandled SQLException. diff --git a/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java b/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java index e2b19303c..5339a177e 100644 --- a/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java +++ b/wrapper/src/test/java/integration/container/tests/AdvancedPerformanceTest.java @@ -29,7 +29,6 @@ import integration.container.ConnectionStringHelper; import integration.container.TestDriverProvider; import integration.container.TestEnvironment; -import integration.container.aurora.TestAuroraHostListProvider; import integration.container.aurora.TestPluginServiceImpl; import integration.container.condition.DisableOnTestFeature; import integration.container.condition.EnableOnTestFeature; @@ -66,6 +65,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; import software.amazon.jdbc.PropertyDefinition; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.efm.HostMonitorThreadContainer; import software.amazon.jdbc.plugin.efm2.HostMonitorServiceImpl; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; diff --git a/wrapper/src/test/java/integration/container/tests/AutoscalingTests.java b/wrapper/src/test/java/integration/container/tests/AutoscalingTests.java index 307e00bb9..28a6c63e4 100644 --- a/wrapper/src/test/java/integration/container/tests/AutoscalingTests.java +++ b/wrapper/src/test/java/integration/container/tests/AutoscalingTests.java @@ -52,7 +52,7 @@ import software.amazon.jdbc.HikariPoolConfigurator; import software.amazon.jdbc.HikariPooledConnectionProvider; import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; import software.amazon.jdbc.plugin.readwritesplitting.ReadWriteSplittingPlugin; @@ -104,7 +104,7 @@ public void test_pooledConnectionAutoScaling_setReadOnlyOnOldConnection() final Properties props = getProps(); final long topologyRefreshRateMs = 5000; ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.set(props, "leastConnections"); - AuroraHostListProvider.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.set(props, + RdsHostListProvider.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.set(props, Long.toString(topologyRefreshRateMs)); final TestEnvironmentInfo testInfo = TestEnvironment.getCurrent().getInfo(); @@ -186,7 +186,7 @@ public void test_pooledConnectionAutoScaling_failoverFromDeletedReader() final Properties props = getPropsWithFailover(); final long topologyRefreshRateMs = 5000; ReadWriteSplittingPlugin.READER_HOST_SELECTOR_STRATEGY.set(props, "leastConnections"); - AuroraHostListProvider.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.set(props, + RdsHostListProvider.CLUSTER_TOPOLOGY_REFRESH_RATE_MS.set(props, Long.toString(topologyRefreshRateMs)); final TestEnvironmentInfo testInfo = TestEnvironment.getCurrent().getInfo(); diff --git a/wrapper/src/test/java/integration/container/tests/FailoverTest.java b/wrapper/src/test/java/integration/container/tests/FailoverTest.java index 758e5ca5f..241499f63 100644 --- a/wrapper/src/test/java/integration/container/tests/FailoverTest.java +++ b/wrapper/src/test/java/integration/container/tests/FailoverTest.java @@ -62,7 +62,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.ds.AwsWrapperDataSource; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; import software.amazon.jdbc.plugin.failover.TransactionStateUnknownSQLException; import software.amazon.jdbc.util.SqlState; @@ -688,7 +688,7 @@ protected Properties initDefaultProxiedProps() { // Some tests temporarily disable connectivity for 5 seconds. The socket timeout needs to be less than this to // trigger driver failover. PropertyDefinition.SOCKET_TIMEOUT.set(props, "2000"); - AuroraHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set( + RdsHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set( props, "?." + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointSuffix() + ":" + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointPort()); diff --git a/wrapper/src/test/java/integration/container/tests/PerformanceTest.java b/wrapper/src/test/java/integration/container/tests/PerformanceTest.java index 1c521f617..5993a716b 100644 --- a/wrapper/src/test/java/integration/container/tests/PerformanceTest.java +++ b/wrapper/src/test/java/integration/container/tests/PerformanceTest.java @@ -61,7 +61,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.OpenedConnectionTracker; import software.amazon.jdbc.plugin.efm.HostMonitorThreadContainer; diff --git a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java index b8c96edbe..8b3accba1 100644 --- a/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java +++ b/wrapper/src/test/java/integration/container/tests/ReadWriteSplittingTests.java @@ -73,7 +73,7 @@ import software.amazon.jdbc.HikariPooledConnectionProvider; import software.amazon.jdbc.PluginService; import software.amazon.jdbc.PropertyDefinition; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.plugin.failover.FailoverConnectionPlugin; import software.amazon.jdbc.plugin.failover.FailoverFailedSQLException; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; @@ -122,7 +122,7 @@ public void tearDownEach() { protected static Properties getProxiedPropsWithFailover() { final Properties props = getPropsWithFailover(); - AuroraHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set(props, + RdsHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set(props, "?." + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointSuffix() + ":" + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointPort()); return props; @@ -130,7 +130,7 @@ protected static Properties getProxiedPropsWithFailover() { protected static Properties getProxiedProps() { final Properties props = getProps(); - AuroraHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set(props, + RdsHostListProvider.CLUSTER_INSTANCE_HOST_PATTERN.set(props, "?." + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointSuffix() + ":" + TestEnvironment.getCurrent().getInfo().getProxyDatabaseInfo().getInstanceEndpointPort()); return props; diff --git a/wrapper/src/test/java/software/amazon/jdbc/DialectDetectionTests.java b/wrapper/src/test/java/software/amazon/jdbc/DialectDetectionTests.java index 3b47f12bb..1a5517559 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/DialectDetectionTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/DialectDetectionTests.java @@ -43,13 +43,14 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.dialect.DialectManager; import software.amazon.jdbc.dialect.MariaDbDialect; +import software.amazon.jdbc.dialect.MultiAzClusterMysqlDialect; +import software.amazon.jdbc.dialect.MultiAzClusterPgDialect; import software.amazon.jdbc.dialect.MysqlDialect; import software.amazon.jdbc.dialect.PgDialect; -import software.amazon.jdbc.dialect.RdsMultiAzDbClusterMysqlDialect; -import software.amazon.jdbc.dialect.RdsMultiAzDbClusterPgDialect; import software.amazon.jdbc.dialect.RdsMysqlDialect; import software.amazon.jdbc.dialect.RdsPgDialect; import software.amazon.jdbc.exceptions.ExceptionManager; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.FullServicesContainer; import software.amazon.jdbc.util.storage.StorageService; @@ -70,10 +71,10 @@ public class DialectDetectionTests { @Mock private Statement mockStatement; @Mock private ResultSet mockSuccessResultSet; @Mock private ResultSet mockFailResultSet; + @Mock private ResultSetMetaData mockResultSetMetaData; @Mock private HostSpec mockHost; @Mock private ConnectionPluginManager mockPluginManager; @Mock private TargetDriverDialect mockTargetDriverDialect; - @Mock private ResultSetMetaData mockResultSetMetaData; @BeforeEach void setUp() throws SQLException { @@ -83,6 +84,8 @@ void setUp() throws SQLException { when(this.mockServicesContainer.getStorageService()).thenReturn(mockStorageService); when(this.mockConnection.createStatement()).thenReturn(this.mockStatement); when(this.mockHost.getUrl()).thenReturn("url"); + when(this.mockFailResultSet.getMetaData()).thenReturn(mockResultSetMetaData); + when(this.mockResultSetMetaData.getColumnCount()).thenReturn(4); when(this.mockFailResultSet.next()).thenReturn(false); mockPluginManager.plugins = new ArrayList<>(); } @@ -219,7 +222,7 @@ void testUpdateDialectPgToTaz() throws SQLException { final PluginServiceImpl target = getPluginService(LOCALHOST, PG_PROTOCOL); target.setInitialConnectionHostSpec(mockHost); target.updateDialect(mockConnection); - assertEquals(RdsMultiAzDbClusterPgDialect.class, target.dialect.getClass()); + assertEquals(MultiAzClusterPgDialect.class, target.dialect.getClass()); } @Test @@ -272,7 +275,7 @@ void testUpdateDialectMariaToMysqlTaz() throws SQLException { final PluginServiceImpl target = getPluginService(LOCALHOST, MARIA_PROTOCOL); target.setInitialConnectionHostSpec(mockHost); target.updateDialect(mockConnection); - assertEquals(RdsMultiAzDbClusterMysqlDialect.class, target.dialect.getClass()); + assertEquals(MultiAzClusterMysqlDialect.class, target.dialect.getClass()); } @Test diff --git a/wrapper/src/test/java/software/amazon/jdbc/DialectTests.java b/wrapper/src/test/java/software/amazon/jdbc/DialectTests.java index 4170f8556..e3c2df258 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/DialectTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/DialectTests.java @@ -36,10 +36,10 @@ import software.amazon.jdbc.dialect.AuroraMysqlDialect; import software.amazon.jdbc.dialect.AuroraPgDialect; import software.amazon.jdbc.dialect.MariaDbDialect; +import software.amazon.jdbc.dialect.MultiAzClusterMysqlDialect; +import software.amazon.jdbc.dialect.MultiAzClusterPgDialect; import software.amazon.jdbc.dialect.MysqlDialect; import software.amazon.jdbc.dialect.PgDialect; -import software.amazon.jdbc.dialect.RdsMultiAzDbClusterMysqlDialect; -import software.amazon.jdbc.dialect.RdsMultiAzDbClusterPgDialect; import software.amazon.jdbc.dialect.RdsMysqlDialect; import software.amazon.jdbc.dialect.RdsPgDialect; @@ -51,11 +51,11 @@ public class DialectTests { @Mock private ResultSetMetaData mockResultSetMetaData; private final MysqlDialect mysqlDialect = new MysqlDialect(); private final RdsMysqlDialect rdsMysqlDialect = new RdsMysqlDialect(); - private final RdsMultiAzDbClusterMysqlDialect rdsTazMysqlDialect = new RdsMultiAzDbClusterMysqlDialect(); + private final MultiAzClusterMysqlDialect rdsTazMysqlDialect = new MultiAzClusterMysqlDialect(); private final AuroraMysqlDialect auroraMysqlDialect = new AuroraMysqlDialect(); private final PgDialect pgDialect = new PgDialect(); private final RdsPgDialect rdsPgDialect = new RdsPgDialect(); - private final RdsMultiAzDbClusterPgDialect rdsTazPgDialect = new RdsMultiAzDbClusterPgDialect(); + private final MultiAzClusterPgDialect rdsTazPgDialect = new MultiAzClusterPgDialect(); private final AuroraPgDialect auroraPgDialect = new AuroraPgDialect(); private final MariaDbDialect mariaDbDialect = new MariaDbDialect(); private AutoCloseable closeable; diff --git a/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java b/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java index db9a072a1..5ef1235ad 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java +++ b/wrapper/src/test/java/software/amazon/jdbc/PluginServiceImplTests.java @@ -62,6 +62,7 @@ import software.amazon.jdbc.exceptions.ExceptionManager; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.profile.ConfigurationProfile; import software.amazon.jdbc.profile.ConfigurationProfileBuilder; import software.amazon.jdbc.states.SessionStateService; diff --git a/wrapper/src/test/java/software/amazon/jdbc/RoundRobinHostSelectorTest.java b/wrapper/src/test/java/software/amazon/jdbc/RoundRobinHostSelectorTest.java index 7a523a3e7..3f61a1ec8 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/RoundRobinHostSelectorTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/RoundRobinHostSelectorTest.java @@ -27,12 +27,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import software.amazon.jdbc.HostRole; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.HostSpecBuilder; -import software.amazon.jdbc.RoundRobinHostSelector; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; -import software.amazon.jdbc.util.HostSelectorUtils; public class RoundRobinHostSelectorTest { private static final int TEST_PORT = 5432; diff --git a/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsHostListProviderTest.java b/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsHostListProviderTest.java index cc337d2a3..991c0734b 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsHostListProviderTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsHostListProviderTest.java @@ -17,7 +17,6 @@ package software.amazon.jdbc.hostlistprovider; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -25,19 +24,12 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atMostOnce; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.mysql.cj.exceptions.WrongArgumentException; import java.sql.Connection; -import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLSyntaxErrorException; -import java.sql.Statement; -import java.sql.Timestamp; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -46,21 +38,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.dialect.Dialect; +import software.amazon.jdbc.dialect.TopologyDialect; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; import software.amazon.jdbc.hostlistprovider.RdsHostListProvider.FetchTopologyResult; import software.amazon.jdbc.util.FullServicesContainer; +import software.amazon.jdbc.util.Pair; import software.amazon.jdbc.util.events.EventPublisher; import software.amazon.jdbc.util.storage.StorageService; import software.amazon.jdbc.util.storage.TestStorageServiceImpl; @@ -70,14 +60,13 @@ class RdsHostListProviderTest { private RdsHostListProvider rdsHostListProvider; @Mock private Connection mockConnection; - @Mock private Statement mockStatement; - @Mock private ResultSet mockResultSet; @Mock private FullServicesContainer mockServicesContainer; @Mock private PluginService mockPluginService; @Mock private HostListProviderService mockHostListProviderService; + @Mock private HostSpecBuilder mockHostSpecBuilder; @Mock private EventPublisher mockEventPublisher; - @Mock Dialect mockTopologyAwareDialect; - @Captor private ArgumentCaptor queryCaptor; + @Mock private TopologyUtils mockTopologyUtils; + @Mock private TopologyDialect mockDialect; private AutoCloseable closeable; private final HostSpec currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) @@ -92,12 +81,12 @@ void setUp() throws SQLException { storageService = new TestStorageServiceImpl(mockEventPublisher); when(mockServicesContainer.getHostListProviderService()).thenReturn(mockHostListProviderService); when(mockServicesContainer.getStorageService()).thenReturn(storageService); + when(mockServicesContainer.getPluginService()).thenReturn(mockPluginService); when(mockPluginService.getCurrentConnection()).thenReturn(mockConnection); when(mockPluginService.connect(any(HostSpec.class), any(Properties.class))).thenReturn(mockConnection); when(mockPluginService.getCurrentHostSpec()).thenReturn(currentHostSpec); - when(mockConnection.createStatement()).thenReturn(mockStatement); - when(mockStatement.executeQuery(queryCaptor.capture())).thenReturn(mockResultSet); - when(mockHostListProviderService.getDialect()).thenReturn(mockTopologyAwareDialect); + when(mockPluginService.getHostSpecBuilder()).thenReturn(mockHostSpecBuilder); + when(mockHostListProviderService.getDialect()).thenReturn(mockDialect); when(mockHostListProviderService.getHostSpecBuilder()) .thenReturn(new HostSpecBuilder(new SimpleHostAvailabilityStrategy())); when(mockHostListProviderService.getCurrentConnection()).thenReturn(mockConnection); @@ -111,10 +100,7 @@ void tearDown() throws Exception { private RdsHostListProvider getRdsHostListProvider(String originalUrl) throws SQLException { RdsHostListProvider provider = new RdsHostListProvider( - new Properties(), - originalUrl, - mockServicesContainer, - "foo", "bar", "baz"); + mockTopologyUtils, new Properties(), originalUrl, mockServicesContainer); provider.init(); return provider; } @@ -141,7 +127,8 @@ void testGetTopology_withForceUpdate_returnsUpdatedTopology() throws SQLExceptio final List newHosts = Collections.singletonList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("newHost").build()); - doReturn(newHosts).when(rdsHostListProvider).queryForTopology(mockConnection); + doReturn(newHosts).when(mockTopologyUtils).queryForTopology( + eq(mockConnection), any(HostSpec.class), any(HostSpec.class)); final FetchTopologyResult result = rdsHostListProvider.getTopology(mockConnection, true); verify(rdsHostListProvider, atMostOnce()).queryForTopology(mockConnection); @@ -189,9 +176,8 @@ void testQueryForTopology_withDifferentDriverProtocol() throws SQLException { final List expectedPostgres = Collections.singletonList( new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("postgresql").port(HostSpec.NO_PORT) .role(HostRole.WRITER).availability(HostAvailability.AVAILABLE).weight(0).build()); - when(mockResultSet.next()).thenReturn(true, false); - when(mockResultSet.getBoolean(eq(2))).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("mysql"); + when(mockTopologyUtils.queryForTopology(eq(mockConnection), any(HostSpec.class), any(HostSpec.class))) + .thenReturn(expectedMySQL).thenReturn(expectedPostgres); rdsHostListProvider = getRdsHostListProvider("mysql://url/"); @@ -199,24 +185,11 @@ void testQueryForTopology_withDifferentDriverProtocol() throws SQLException { List hosts = rdsHostListProvider.queryForTopology(mockConnection); assertEquals(expectedMySQL, hosts); - when(mockResultSet.next()).thenReturn(true, false); - when(mockResultSet.getString(eq(1))).thenReturn("postgresql"); - rdsHostListProvider = getRdsHostListProvider("postgresql://url/"); hosts = rdsHostListProvider.queryForTopology(mockConnection); assertEquals(expectedPostgres, hosts); } - @Test - void testQueryForTopology_queryResultsInException() throws SQLException { - rdsHostListProvider = getRdsHostListProvider("protocol://url/"); - when(mockStatement.executeQuery(queryCaptor.capture())).thenThrow(new SQLSyntaxErrorException()); - - assertThrows( - SQLException.class, - () -> rdsHostListProvider.queryForTopology(mockConnection)); - } - @Test void testGetCachedTopology_returnStoredTopology() throws SQLException { rdsHostListProvider = getRdsHostListProvider("jdbc:someprotocol://url"); @@ -228,53 +201,10 @@ void testGetCachedTopology_returnStoredTopology() throws SQLException { assertEquals(expected, result); } - @Test - void testTopologyCache() throws SQLException { - - RdsHostListProvider provider1 = Mockito.spy(getRdsHostListProvider("jdbc:something://cluster-a.domain.com/")); - provider1.init(); - final List topologyClusterA = Arrays.asList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-1.domain.com").port(HostSpec.NO_PORT).role(HostRole.WRITER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-2.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-3.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build()); - - doReturn(topologyClusterA) - .when(provider1).queryForTopology(any(Connection.class)); - - assertEquals(0, storageService.size(Topology.class)); - - final List topologyProvider1 = provider1.refresh(mock(Connection.class)); - assertEquals(topologyClusterA, topologyProvider1); - - RdsHostListProvider provider2 = Mockito.spy(getRdsHostListProvider("jdbc:something://cluster-b.domain.com/")); - assertNotNull(provider2.getStoredTopology()); - - final List topologyClusterB = Arrays.asList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-1.domain.com").port(HostSpec.NO_PORT).role(HostRole.WRITER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-2.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-3.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build()); - doReturn(topologyClusterB).when(provider2).queryForTopology(any(Connection.class)); - - List topologyProvider2 = provider2.refresh(mock(Connection.class)); - assertNotEquals(topologyClusterB, topologyProvider2); - - topologyProvider2 = provider2.forceRefresh(mock(Connection.class)); - assertEquals(topologyClusterB, topologyProvider2); - - assertEquals(1, storageService.size(Topology.class)); - } - @Test void testIdentifyConnectionWithInvalidNodeIdQuery() throws SQLException { rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - when(mockResultSet.next()).thenReturn(false); assertThrows(SQLException.class, () -> rdsHostListProvider.identifyConnection(mockConnection)); when(mockConnection.createStatement()).thenThrow(new SQLException("exception")); @@ -284,11 +214,10 @@ void testIdentifyConnectionWithInvalidNodeIdQuery() throws SQLException { @Test void testIdentifyConnectionNullTopology() throws SQLException { rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - rdsHostListProvider.clusterInstanceTemplate = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("?.pattern").build(); + rdsHostListProvider.instanceTemplate = + new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("?.pattern").build(); - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-1"); + when(mockTopologyUtils.getInstanceId(mockConnection)).thenReturn(Pair.create("instance-1", "instance-1")); doReturn(null).when(rdsHostListProvider).refresh(mockConnection); doReturn(null).when(rdsHostListProvider).forceRefresh(mockConnection); @@ -305,8 +234,7 @@ void testIdentifyConnectionHostNotInTopology() throws SQLException { .build()); rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-1"); + when(mockTopologyUtils.getInstanceId(mockConnection)).thenReturn(Pair.create("instance-1", "instance-1")); doReturn(cachedTopology).when(rdsHostListProvider).refresh(mockConnection); doReturn(cachedTopology).when(rdsHostListProvider).forceRefresh(mockConnection); @@ -324,8 +252,7 @@ void testIdentifyConnectionHostInTopology() throws SQLException { final List cachedTopology = Collections.singletonList(expectedHost); rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-a-1"); + when(mockTopologyUtils.getInstanceId(mockConnection)).thenReturn(Pair.create("instance-a-1", "instance-a-1")); doReturn(cachedTopology).when(rdsHostListProvider).refresh(mockConnection); doReturn(cachedTopology).when(rdsHostListProvider).forceRefresh(mockConnection); @@ -333,122 +260,4 @@ void testIdentifyConnectionHostInTopology() throws SQLException { assertEquals("instance-a-1.xyz.us-east-2.rds.amazonaws.com", actual.getHost()); assertEquals("instance-a-1", actual.getHostId()); } - - @Test - void testGetTopology_StaleRecord() throws SQLException { - rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - rdsHostListProvider.isInitialized = true; - - final String hostName1 = "hostName1"; - final String hostName2 = "hostName2"; - final Double cpuUtilization = 11.1D; - final Double nodeLag = 0.123D; - final Timestamp firstTimestamp = Timestamp.from(Instant.now()); - final Timestamp secondTimestamp = new Timestamp(firstTimestamp.getTime() + 100); - when(mockResultSet.next()).thenReturn(true, true, false); - when(mockResultSet.getString(1)).thenReturn(hostName1).thenReturn(hostName2); - when(mockResultSet.getBoolean(2)).thenReturn(true).thenReturn(true); - when(mockResultSet.getDouble(3)).thenReturn(cpuUtilization).thenReturn(cpuUtilization); - when(mockResultSet.getDouble(4)).thenReturn(nodeLag).thenReturn(nodeLag); - when(mockResultSet.getTimestamp(5)).thenReturn(firstTimestamp).thenReturn(secondTimestamp); - long weight = Math.round(nodeLag) * 100L + Math.round(cpuUtilization); - final HostSpec expectedWriter = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host(hostName2) - .port(-1) - .role(HostRole.WRITER) - .availability(HostAvailability.AVAILABLE) - .weight(weight) - .lastUpdateTime(secondTimestamp) - .build(); - - final FetchTopologyResult result = rdsHostListProvider.getTopology(mockConnection, true); - verify(rdsHostListProvider, atMostOnce()).queryForTopology(mockConnection); - assertEquals(1, result.hosts.size()); - assertEquals(expectedWriter, result.hosts.get(0)); - } - - @Test - void testGetTopology_InvalidLastUpdatedTimestamp() throws SQLException { - rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - rdsHostListProvider.isInitialized = true; - - final String hostName = "hostName"; - final Double cpuUtilization = 11.1D; - final Double nodeLag = 0.123D; - when(mockResultSet.next()).thenReturn(true, false); - when(mockResultSet.getString(1)).thenReturn(hostName); - when(mockResultSet.getBoolean(2)).thenReturn(true); - when(mockResultSet.getDouble(3)).thenReturn(cpuUtilization); - when(mockResultSet.getDouble(4)).thenReturn(nodeLag); - when(mockResultSet.getTimestamp(5)).thenThrow(WrongArgumentException.class); - - final FetchTopologyResult result = rdsHostListProvider.getTopology(mockConnection, true); - verify(rdsHostListProvider, atMostOnce()).queryForTopology(mockConnection); - - final String expectedLastUpdatedTimeStampRounded = Timestamp.from(Instant.now()).toString().substring(0, 16); - assertEquals(1, result.hosts.size()); - assertEquals( - expectedLastUpdatedTimeStampRounded, - result.hosts.get(0).getLastUpdateTime().toString().substring(0, 16)); - } - - @Test - void testGetTopology_returnsLatestWriter() throws SQLException { - rdsHostListProvider = Mockito.spy(getRdsHostListProvider("jdbc:someprotocol://url")); - rdsHostListProvider.isInitialized = true; - - HostSpec expectedWriterHost = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("expectedWriterHost") - .role(HostRole.WRITER) - .lastUpdateTime(Timestamp.valueOf("3000-01-01 00:00:00")) - .build(); - - HostSpec unexpectedWriterHost0 = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("unexpectedWriterHost0") - .role(HostRole.WRITER) - .lastUpdateTime(Timestamp.valueOf("1000-01-01 00:00:00")) - .build(); - - HostSpec unexpectedWriterHost1 = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("unexpectedWriterHost1") - .role(HostRole.WRITER) - .lastUpdateTime(Timestamp.valueOf("2000-01-01 00:00:00")) - .build(); - - HostSpec unexpectedWriterHostWithNullLastUpdateTime0 = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("unexpectedWriterHostWithNullLastUpdateTime0") - .role(HostRole.WRITER) - .lastUpdateTime(null) - .build(); - - HostSpec unexpectedWriterHostWithNullLastUpdateTime1 = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("unexpectedWriterHostWithNullLastUpdateTime1") - .role(HostRole.WRITER) - .lastUpdateTime(null) - .build(); - - when(mockResultSet.next()).thenReturn(true, true, true, true, true, false); - - when(mockResultSet.getString(1)).thenReturn( - unexpectedWriterHostWithNullLastUpdateTime0.getHost(), - unexpectedWriterHost0.getHost(), - expectedWriterHost.getHost(), - unexpectedWriterHost1.getHost(), - unexpectedWriterHostWithNullLastUpdateTime1.getHost()); - when(mockResultSet.getBoolean(2)).thenReturn(true, true, true, true, true); - when(mockResultSet.getFloat(3)).thenReturn((float) 0, (float) 0, (float) 0, (float) 0, (float) 0); - when(mockResultSet.getFloat(4)).thenReturn((float) 0, (float) 0, (float) 0, (float) 0, (float) 0); - when(mockResultSet.getTimestamp(5)).thenReturn( - unexpectedWriterHostWithNullLastUpdateTime0.getLastUpdateTime(), - unexpectedWriterHost0.getLastUpdateTime(), - expectedWriterHost.getLastUpdateTime(), - unexpectedWriterHost1.getLastUpdateTime(), - unexpectedWriterHostWithNullLastUpdateTime1.getLastUpdateTime() - ); - - final FetchTopologyResult result = rdsHostListProvider.getTopology(mockConnection, true); - verify(rdsHostListProvider, atMostOnce()).queryForTopology(mockConnection); - - assertEquals(expectedWriterHost.getHost(), result.hosts.get(0).getHost()); - } } diff --git a/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProviderTest.java b/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProviderTest.java deleted file mode 100644 index 5c0343487..000000000 --- a/wrapper/src/test/java/software/amazon/jdbc/hostlistprovider/RdsMultiAzDbClusterListProviderTest.java +++ /dev/null @@ -1,311 +0,0 @@ -/* - * 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.hostlistprovider; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atMostOnce; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.sql.Connection; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLSyntaxErrorException; -import java.sql.Statement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; -import software.amazon.jdbc.HostListProviderService; -import software.amazon.jdbc.HostRole; -import software.amazon.jdbc.HostSpec; -import software.amazon.jdbc.HostSpecBuilder; -import software.amazon.jdbc.PluginService; -import software.amazon.jdbc.dialect.Dialect; -import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; -import software.amazon.jdbc.hostlistprovider.RdsHostListProvider.FetchTopologyResult; -import software.amazon.jdbc.util.FullServicesContainer; -import software.amazon.jdbc.util.events.EventPublisher; -import software.amazon.jdbc.util.storage.StorageService; -import software.amazon.jdbc.util.storage.TestStorageServiceImpl; - -class RdsMultiAzDbClusterListProviderTest { - private StorageService storageService; - private RdsMultiAzDbClusterListProvider rdsMazDbClusterHostListProvider; - - @Mock private Connection mockConnection; - @Mock private Statement mockStatement; - @Mock private ResultSet mockResultSet; - @Mock private FullServicesContainer mockServicesContainer; - @Mock private PluginService mockPluginService; - @Mock private HostListProviderService mockHostListProviderService; - @Mock private EventPublisher mockEventPublisher; - @Mock Dialect mockTopologyAwareDialect; - @Captor private ArgumentCaptor queryCaptor; - - private AutoCloseable closeable; - private final HostSpec currentHostSpec = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("foo").port(1234).build(); - private final List hosts = Arrays.asList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("host1").build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("host2").build()); - - @BeforeEach - void setUp() throws SQLException { - closeable = MockitoAnnotations.openMocks(this); - storageService = new TestStorageServiceImpl(mockEventPublisher); - when(mockServicesContainer.getHostListProviderService()).thenReturn(mockHostListProviderService); - when(mockServicesContainer.getStorageService()).thenReturn(storageService); - when(mockPluginService.getCurrentConnection()).thenReturn(mockConnection); - when(mockPluginService.connect(any(HostSpec.class), any(Properties.class))).thenReturn(mockConnection); - when(mockPluginService.getCurrentHostSpec()).thenReturn(currentHostSpec); - when(mockConnection.createStatement()).thenReturn(mockStatement); - when(mockStatement.executeQuery(queryCaptor.capture())).thenReturn(mockResultSet); - when(mockHostListProviderService.getDialect()).thenReturn(mockTopologyAwareDialect); - when(mockHostListProviderService.getHostSpecBuilder()) - .thenReturn(new HostSpecBuilder(new SimpleHostAvailabilityStrategy())); - } - - @AfterEach - void tearDown() throws Exception { - storageService.clearAll(); - closeable.close(); - } - - private RdsMultiAzDbClusterListProvider getRdsMazDbClusterHostListProvider(String originalUrl) throws SQLException { - RdsMultiAzDbClusterListProvider provider = new RdsMultiAzDbClusterListProvider( - new Properties(), - originalUrl, - mockServicesContainer, - "foo", - "bar", - "baz", - "fang", - "li"); - provider.init(); - // provider.clusterId = "1"; - return provider; - } - - @Test - void testGetTopology_returnCachedTopology() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("protocol://url/")); - final List expected = hosts; - storageService.set(rdsMazDbClusterHostListProvider.clusterId, new Topology(expected)); - - final FetchTopologyResult result = rdsMazDbClusterHostListProvider.getTopology(mockConnection, false); - assertEquals(expected, result.hosts); - assertEquals(2, result.hosts.size()); - verify(rdsMazDbClusterHostListProvider, never()).queryForTopology(mockConnection); - } - - @Test - void testGetTopology_withForceUpdate_returnsUpdatedTopology() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - rdsMazDbClusterHostListProvider.isInitialized = true; - - storageService.set(rdsMazDbClusterHostListProvider.clusterId, new Topology(hosts)); - - final List newHosts = Collections.singletonList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("newHost").build()); - doReturn(newHosts).when(rdsMazDbClusterHostListProvider).queryForTopology(mockConnection); - - final FetchTopologyResult result = rdsMazDbClusterHostListProvider.getTopology(mockConnection, true); - verify(rdsMazDbClusterHostListProvider, atMostOnce()).queryForTopology(mockConnection); - assertEquals(1, result.hosts.size()); - assertEquals(newHosts, result.hosts); - } - - @Test - void testGetTopology_noForceUpdate_queryReturnsEmptyHostList() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - rdsMazDbClusterHostListProvider.clusterId = "cluster-id"; - rdsMazDbClusterHostListProvider.isInitialized = true; - - final List expected = hosts; - storageService.set(rdsMazDbClusterHostListProvider.clusterId, new Topology(expected)); - - doReturn(new ArrayList<>()).when(rdsMazDbClusterHostListProvider).queryForTopology(mockConnection); - - final FetchTopologyResult result = rdsMazDbClusterHostListProvider.getTopology(mockConnection, false); - verify(rdsMazDbClusterHostListProvider, atMostOnce()).queryForTopology(mockConnection); - assertEquals(2, result.hosts.size()); - assertEquals(expected, result.hosts); - } - - @Test - void testGetTopology_withForceUpdate_returnsInitialHostList() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - rdsMazDbClusterHostListProvider.clear(); - - doReturn(new ArrayList<>()).when(rdsMazDbClusterHostListProvider).queryForTopology(mockConnection); - - final FetchTopologyResult result = rdsMazDbClusterHostListProvider.getTopology(mockConnection, true); - verify(rdsMazDbClusterHostListProvider, atMostOnce()).queryForTopology(mockConnection); - assertNotNull(result.hosts); - assertEquals( - Collections.singletonList(new HostSpecBuilder(new SimpleHostAvailabilityStrategy()).host("url").build()), - result.hosts); - } - - @Test - void testQueryForTopology_queryResultsInException() throws SQLException { - rdsMazDbClusterHostListProvider = getRdsMazDbClusterHostListProvider("protocol://url/"); - when(mockStatement.executeQuery(queryCaptor.capture())).thenThrow(new SQLSyntaxErrorException()); - - assertThrows( - SQLException.class, - () -> rdsMazDbClusterHostListProvider.queryForTopology(mockConnection)); - } - - @Test - void testGetCachedTopology_returnCachedTopology() throws SQLException { - rdsMazDbClusterHostListProvider = getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url"); - - final List expected = hosts; - storageService.set(rdsMazDbClusterHostListProvider.clusterId, new Topology(expected)); - - final List result = rdsMazDbClusterHostListProvider.getStoredTopology(); - assertEquals(expected, result); - } - - @Test - void testTopologyCache() throws SQLException { - - RdsMultiAzDbClusterListProvider provider1 = - Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:something://cluster-a.domain.com/")); - provider1.init(); - final List topologyClusterA = Arrays.asList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-1.domain.com").port(HostSpec.NO_PORT).role(HostRole.WRITER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-2.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-3.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build()); - - doReturn(topologyClusterA) - .when(provider1).queryForTopology(any(Connection.class)); - - assertEquals(0, storageService.size(Topology.class)); - - final List topologyProvider1 = provider1.refresh(Mockito.mock(Connection.class)); - assertEquals(topologyClusterA, topologyProvider1); - - RdsMultiAzDbClusterListProvider provider2 = - Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:something://cluster-b.domain.com/")); - assertNotNull(provider2.getStoredTopology()); - - final List topologyClusterB = Arrays.asList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-1.domain.com").port(HostSpec.NO_PORT).role(HostRole.WRITER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-2.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build(), - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-b-3.domain.com").port(HostSpec.NO_PORT).role(HostRole.READER).build()); - doReturn(topologyClusterB).when(provider2).queryForTopology(any(Connection.class)); - - List topologyProvider2 = provider2.refresh(Mockito.mock(Connection.class)); - assertNotEquals(topologyClusterB, topologyProvider2); - - topologyProvider2 = provider2.forceRefresh(Mockito.mock(Connection.class)); - assertEquals(topologyClusterB, topologyProvider2); - - assertEquals(1, storageService.size(Topology.class)); - } - - @Test - void testIdentifyConnectionWithInvalidNodeIdQuery() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - - when(mockResultSet.next()).thenReturn(false); - assertThrows(SQLException.class, () -> rdsMazDbClusterHostListProvider.identifyConnection(mockConnection)); - - when(mockConnection.createStatement()).thenThrow(new SQLException("exception")); - assertThrows(SQLException.class, () -> rdsMazDbClusterHostListProvider.identifyConnection(mockConnection)); - } - - @Test - void testIdentifyConnectionNullTopology() throws SQLException { - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - rdsMazDbClusterHostListProvider.clusterInstanceTemplate = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("?.pattern").build(); - - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-1"); - doReturn(null).when(rdsMazDbClusterHostListProvider).refresh(mockConnection); - doReturn(null).when(rdsMazDbClusterHostListProvider).forceRefresh(mockConnection); - - assertNull(rdsMazDbClusterHostListProvider.identifyConnection(mockConnection)); - } - - @Test - void testIdentifyConnectionHostNotInTopology() throws SQLException { - final List cachedTopology = Collections.singletonList( - new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-1.xyz.us-east-2.rds.amazonaws.com") - .port(HostSpec.NO_PORT) - .role(HostRole.WRITER) - .build()); - - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-1"); - doReturn(cachedTopology).when(rdsMazDbClusterHostListProvider).refresh(mockConnection); - doReturn(cachedTopology).when(rdsMazDbClusterHostListProvider).forceRefresh(mockConnection); - - assertNull(rdsMazDbClusterHostListProvider.identifyConnection(mockConnection)); - } - - @Test - void testIdentifyConnectionHostInTopology() throws SQLException { - final HostSpec expectedHost = new HostSpecBuilder(new SimpleHostAvailabilityStrategy()) - .host("instance-a-1.xyz.us-east-2.rds.amazonaws.com") - .hostId("instance-a-1") - .port(HostSpec.NO_PORT) - .role(HostRole.WRITER) - .build(); - final List cachedTopology = Collections.singletonList(expectedHost); - - rdsMazDbClusterHostListProvider = Mockito.spy(getRdsMazDbClusterHostListProvider("jdbc:someprotocol://url")); - when(mockResultSet.next()).thenReturn(true); - when(mockResultSet.getString(eq(1))).thenReturn("instance-a-1"); - doReturn(cachedTopology).when(rdsMazDbClusterHostListProvider).refresh(mockConnection); - doReturn(cachedTopology).when(rdsMazDbClusterHostListProvider).forceRefresh(mockConnection); - - final HostSpec actual = rdsMazDbClusterHostListProvider.identifyConnection(mockConnection); - assertEquals("instance-a-1.xyz.us-east-2.rds.amazonaws.com", actual.getHost()); - assertEquals("instance-a-1", actual.getHostId()); - } - -} diff --git a/wrapper/src/test/java/software/amazon/jdbc/mock/TestPluginOne.java b/wrapper/src/test/java/software/amazon/jdbc/mock/TestPluginOne.java index 9ca4c86dd..523d3be59 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/mock/TestPluginOne.java +++ b/wrapper/src/test/java/software/amazon/jdbc/mock/TestPluginOne.java @@ -27,7 +27,6 @@ import java.util.Properties; import java.util.Set; import software.amazon.jdbc.ConnectionPlugin; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; @@ -35,6 +34,7 @@ import software.amazon.jdbc.NodeChangeOptions; import software.amazon.jdbc.OldConnectionSuggestedAction; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; public class TestPluginOne implements ConnectionPlugin { diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPluginTest.java index 1fb98265e..2c72ed243 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/failover/FailoverConnectionPluginTest.java @@ -49,7 +49,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; @@ -58,7 +57,8 @@ import software.amazon.jdbc.PluginService; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; -import software.amazon.jdbc.hostlistprovider.AuroraHostListProvider; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; +import software.amazon.jdbc.hostlistprovider.RdsHostListProvider; import software.amazon.jdbc.targetdriverdialect.TargetDriverDialect; import software.amazon.jdbc.util.FullServicesContainer; import software.amazon.jdbc.util.RdsUrlType; @@ -87,7 +87,7 @@ class FailoverConnectionPluginTest { @Mock Connection mockConnection; @Mock HostSpec mockHostSpec; @Mock HostListProviderService mockHostListProviderService; - @Mock AuroraHostListProvider mockHostListProvider; + @Mock RdsHostListProvider mockHostListProvider; @Mock JdbcCallable mockInitHostProviderFunc; @Mock ReaderFailoverHandler mockReaderFailoverHandler; @Mock WriterFailoverHandler mockWriterFailoverHandler; diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessConnectionPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessConnectionPluginTest.java index 411233100..e6e498a41 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessConnectionPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessConnectionPluginTest.java @@ -35,7 +35,6 @@ import org.mockito.MockitoAnnotations; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; -import software.amazon.jdbc.HostListProvider; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; @@ -45,6 +44,7 @@ import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.dialect.PgDialect; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProvider; public class LimitlessConnectionPluginTest { diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterServiceImplTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterServiceImplTest.java index 7c12f83fb..81559231c 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterServiceImplTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/limitless/LimitlessRouterServiceImplTest.java @@ -39,7 +39,6 @@ import org.mockito.MockitoAnnotations; import software.amazon.jdbc.ConnectionProvider; import software.amazon.jdbc.HighestWeightHostSelector; -import software.amazon.jdbc.HostListProvider; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; @@ -48,6 +47,7 @@ import software.amazon.jdbc.WeightedRandomHostSelector; import software.amazon.jdbc.hostavailability.HostAvailability; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProvider; import software.amazon.jdbc.util.FullServicesContainer; import software.amazon.jdbc.util.FullServicesContainerImpl; import software.amazon.jdbc.util.events.EventPublisher; diff --git a/wrapper/src/test/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPluginTest.java b/wrapper/src/test/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPluginTest.java index 976fcf2ea..7fea814ad 100644 --- a/wrapper/src/test/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPluginTest.java +++ b/wrapper/src/test/java/software/amazon/jdbc/plugin/readwritesplitting/ReadWriteSplittingPluginTest.java @@ -45,7 +45,6 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import software.amazon.jdbc.HostListProviderService; import software.amazon.jdbc.HostRole; import software.amazon.jdbc.HostSpec; import software.amazon.jdbc.HostSpecBuilder; @@ -56,6 +55,7 @@ import software.amazon.jdbc.PropertyDefinition; import software.amazon.jdbc.dialect.Dialect; import software.amazon.jdbc.hostavailability.SimpleHostAvailabilityStrategy; +import software.amazon.jdbc.hostlistprovider.HostListProviderService; import software.amazon.jdbc.plugin.failover.FailoverSuccessSQLException; import software.amazon.jdbc.util.SqlState; diff --git a/wrapper/src/test/resources/hibernate_files/DataSourceTest.java b/wrapper/src/test/resources/hibernate_files/DataSourceTest.java index 624114400..38a6b1c78 100644 --- a/wrapper/src/test/resources/hibernate_files/DataSourceTest.java +++ b/wrapper/src/test/resources/hibernate_files/DataSourceTest.java @@ -4,6 +4,9 @@ */ package org.hibernate.orm.test.datasource; +import static org.hibernate.internal.util.StringHelper.split; +import static org.junit.jupiter.api.Assertions.assertTrue; + import jakarta.persistence.Entity; import jakarta.persistence.Id; import org.hibernate.cfg.Environment; @@ -21,10 +24,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; - -import static org.hibernate.internal.util.StringHelper.split; -import static org.junit.jupiter.api.Assertions.assertTrue; - @Jpa(annotatedClasses = DataSourceTest.TestEntity.class, integrationSettings = @Setting(name = JdbcSettings.CONNECTION_PROVIDER, value = "org.hibernate.orm.test.datasource.TestDataSourceConnectionProvider")) diff --git a/wrapper/src/test/resources/hibernate_files/PostgreSQLCastingIntervalSecondJdbcType.java b/wrapper/src/test/resources/hibernate_files/PostgreSQLCastingIntervalSecondJdbcType.java index 7dcd98188..55f5386bf 100644 --- a/wrapper/src/test/resources/hibernate_files/PostgreSQLCastingIntervalSecondJdbcType.java +++ b/wrapper/src/test/resources/hibernate_files/PostgreSQLCastingIntervalSecondJdbcType.java @@ -9,7 +9,6 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; - import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; diff --git a/wrapper/src/test/resources/hibernate_files/PostgresIntervalSecondTest.java b/wrapper/src/test/resources/hibernate_files/PostgresIntervalSecondTest.java index f07faccac..065b83fb3 100644 --- a/wrapper/src/test/resources/hibernate_files/PostgresIntervalSecondTest.java +++ b/wrapper/src/test/resources/hibernate_files/PostgresIntervalSecondTest.java @@ -6,33 +6,30 @@ import static org.assertj.core.api.Assertions.assertThat; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; import java.time.Duration; - import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.type.PostgreSQLIntervalSecondJdbcType; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.type.SqlTypes; -import org.hibernate.type.descriptor.jdbc.JdbcType; -import org.hibernate.type.descriptor.jdbc.NumericJdbcType; -import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; - import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.NumericJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - /** * Test to see if using `@org.hibernate.annotations.JdbcTypeCode` or `@org.hibernate.annotations.JdbcType` * will override a default JdbcType set by a {@link AvailableSettings#PREFERRED_DURATION_JDBC_TYPE config property}. diff --git a/wrapper/src/test/resources/hibernate_files/StructEmbeddableArrayTest.java b/wrapper/src/test/resources/hibernate_files/StructEmbeddableArrayTest.java index c7c7faa20..46cc08277 100644 --- a/wrapper/src/test/resources/hibernate_files/StructEmbeddableArrayTest.java +++ b/wrapper/src/test/resources/hibernate_files/StructEmbeddableArrayTest.java @@ -4,6 +4,14 @@ */ package org.hibernate.orm.test.mapping.embeddable; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ParameterMode; +import jakarta.persistence.Tuple; import java.net.URL; import java.sql.Time; import java.sql.Timestamp; @@ -18,7 +26,6 @@ import java.util.List; import java.util.Set; import java.util.UUID; - import org.hibernate.annotations.Struct; import org.hibernate.boot.ResourceStreamLocator; import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; @@ -35,7 +42,6 @@ import org.hibernate.dialect.PostgresPlusDialect; import org.hibernate.procedure.ProcedureCall; import org.hibernate.procedure.ProcedureParameter; - import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; import org.hibernate.testing.orm.domain.gambit.MutableValue; @@ -54,16 +60,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.ParameterMode; -import jakarta.persistence.Tuple; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNull; - @BootstrapServiceRegistry( javaServices = @BootstrapServiceRegistry.JavaService( role = AdditionalMappingContributor.class,